本文主要介紹了GLib的主事件迴圈機制,以期使讀者可以讀懂使用GLib主事件迴圈機制的程式碼。本文程式碼倉庫地址如下:
https://github.com/pandengyang/glib_demo.git
主事件迴圈管理著GLib和GTK+應用程式所有可用事件源。事件可以來自任意數量、任意型別的事件源,例如檔案描述符(普通檔案、管道或套接字)和超時。還可以使用g_source_attach新增新型別的事件源。
事件源會被分配一個優先順序,預設為G_PRIORITY_DEFAULT(0)。小於0意味著優先順序較高,而大於0意味著優先順序較低。優先順序較高的事件始終會在優先順序較低的事件之前得到處理。
甚至可以新增空閒函式併為其分配優先順序。當沒有更高優先順序的事件時,空閒函式就會被排程執行。
GMainLoop表示一個主事件迴圈,由g_main_loop_new建立。新增初始事件源後,將呼叫g_main_loop_run。該函式將不停地檢查事件源中是否有新事件並進行分發。最後,某事件的回撥函式會呼叫g_main_loop_quit退出主迴圈,g_main_loop_run返回。
主事件迴圈的基本用法示例如下:
#include <stdio.h>#include <glib.h> /* timeout functions */gboolean count_down(gpointer data);gboolean cancel_fire(gpointer data);/* idle functions */gboolean say_idle(gpointer data);int main(int argc, char *argv[]){ /* GMainLoop 表示一個主事件迴圈,由g_main_loop_new 建立 * 第一個引數為 GMainContext,為 NULL 表示預設 GMainContext */ GMainLoop *loop = g_main_loop_new(NULL, FALSE); /* 新增超時事件源 */ g_timeout_add(500, count_down, NULL); g_timeout_add(8000, cancel_fire, loop); /* 新增空閒函式,當沒有更高優先順序的事件時, * 空閒函式就會被執行 */ g_idle_add(say_idle, NULL); /* 該函式將不停地檢查事件源中是否有新事件進行分發 * * 在某事件處理函式中呼叫 g_main_loop_quit 將導致 * 該函式退出 */ g_main_loop_run(loop); /* 減少 loop 引用計數,如果引用計數為 0,釋放 loop */ g_main_loop_unref(loop); return 0;}/* 超時事件處理函式 * * 該函式會重複執行直到返回 FALSE, * 與此同時,定時器被銷燬 */gboolean count_down(gpointer data){ static int counter = 10; if (counter < 1) { printf("-----[FIRE]-----\n"); /* 定時器被銷燬,count_down 不會被再次執行 */ return FALSE; } printf("-----[% 4d]-----\n", counter--); /* count_down 在發生下次超時事件時執行 */ return TRUE;}/* 超時事件處理函式 */gboolean cancel_fire(gpointer data){ GMainLoop *loop = data; printf("-----[QUIT]-----\n"); /* 退出主迴圈,g_main_loop_run 返回 */ g_main_loop_quit(loop); return FALSE;}/* 空閒函式 * * 如果該函式返回 FALSE,該事件源將被刪除,該函式不會再次執行 */gboolean say_idle(gpointer data){ printf("-----[IDLE]-----\n"); /* 如果該函式返回 FALSE,該事件源將被刪除, * 該函式不會再次執行 */ return FALSE; /* 如果該函式返回 TRUE * 該函式會在沒有更高優先順序的事件時,再次執行 */ // return TRUE;}
可以遞迴建立GMainLoop例項,GTK+應用程式使用該方法顯示模態對話方塊。注意,事件源與特定的GMainContext關聯,而所有與該GMainContext關聯的主迴圈將從這些事件源中檢查新事件並進行分發。
GMainLoop有一項非凡特性,那就是除了內建事件源外,還可以建立和使用新型事件源。新事件源用於來處理GDK事件。透過從GSource結構體派生來建立新事件源。表示新事件源的結構體的第一個元素為GSource例項,其他元素與新事件源本身相關。透過呼叫g_source_new建立新事件源的例項,建立時需傳遞新事件源結構體的大小和一張函式表,而這張函式表決定了新事件源的行為。
函式表定義如下:
struct GSourceFuncs { gboolean (*prepare) (GSource *source,gint *timeout_); gboolean (*check) (GSource *source); gboolean (*dispatch) (GSource *source, GSourceFunc callback, gpointer user_data); void (*finalize) (GSource *source);};
prepare在poll所有檔案描述符之前呼叫。如果事件源中有事件發生過(無需透過檢視poll呼叫的結果來確定是否有事件發生),就返回TRUE。prepare可返回一個timeout,該值為poll呼叫的超時時間(單位為毫秒)。如果所有事件源返回的timeout均為-1,則poll的超時時間將為-1,否則poll的超時時間為所有事件源返回的timeout值(不包含-1)中的最小值。
check在poll所有檔案描述符之後呼叫。如果事件已發生,就返回TRUE。
當事件源的prepare或check函式返回TRUE後,說明事件源中有事件發生,可呼叫dispatch為事件源分發事件。dispatch函式的引數為回撥函式和使用者資料,回撥函式可為NULL。dispatch函式應呼叫回撥函式,並向其傳遞使用者資料。當需要刪除事件源時,dispatch函式返回G_SOURCE_REMOVE,當需要保留事件源時,dispatch函式返回G_SOURCE_CONTINUE。
新事件源透過兩種方式與GMainContext互動。函式表中的prepare函式設定一個超時時間以確定主迴圈在檢查該事件源之前的最長休眠時間,這個休眠事件用於決定poll的超時事件。此外,事件源還可以使用g_source_add_poll將檔案描述符新增到GMainContext檢查的集合中。
事件迴圈的過程如下:
步驟一(prepare):呼叫各事件源的prepare回撥函式,檢查事件源中是否有事件發生。對於無需poll的事件源(例如:idle事件源),其返回TRUE,表示idle事件已經發生了。對於需要poll的事件源(例如:檔案事件源),其返回FALSE,因為只有在poll檔案之後,才能直到檔案事件是否發生。步驟二(query):獲取實際需要poll的檔案fd。步驟三(check):呼叫poll對fd進行監聽,呼叫各事件源的check回撥函式,檢查事件源是否有事件發生。步驟四(dispatch):若某事件源有事件發生(prepare或check返回TRUE),則呼叫其事件處理函式。下面我們自定義一個IDLE事件源:
netcat -l 127.0.0.1 9999
示例中定義了一個Echo事件源,接受TCP Server傳送給TCP Client的資料,顯示並Echo給TCP Server。程式碼如下:
#include <stdio.h>#include <glib.h>#include <arpa/inet.h>#include <sys/socket.h> /* GSourceEcho */typedef struct GSourceEcho { GSource source; GIOChannel *channel; GPollFD fd;} GSourceEcho;gboolean g_source_echo_prepare(GSource * source, gint * timeout);gboolean g_source_echo_check(GSource * source);gboolean g_source_echo_dispatch(GSource * source, GSourceFunc callback, gpointer user_data);void g_source_echo_finalize(GSource * source);gboolean echo(GIOChannel * channel);/* tcp client */int client_fd;int client(void); int main(int argc, char *argv[]){ if (-1 == client()) { g_print("%s\n", "start client error"); return -1; } GMainLoop *loop = g_main_loop_new(NULL, FALSE); GMainContext *context = g_main_loop_get_context(loop); GSourceFuncs g_source_echo_funcs = { g_source_echo_prepare, g_source_echo_check, g_source_echo_dispatch, g_source_echo_finalize, }; GSource *source = g_source_new(&g_source_echo_funcs, sizeof(GSourceEcho)); GSourceEcho *source_echo = (GSourceEcho *) source; source_echo->channel = g_io_channel_unix_new(client_fd); source_echo->fd.fd = client_fd; source_echo->fd.events = G_IO_IN; g_source_add_poll(source, &source_echo->fd); g_source_set_callback(source, (GSourceFunc) echo, NULL, NULL); g_source_attach(source, context); g_source_unref(source); g_main_loop_run(loop); g_main_context_unref(context); g_main_loop_unref(loop); return 0;}/* 對於檔案描述符源,prepare 通常返回 FALSE, * 因為它必須等呼叫 poll 後才能知道是否需要處理事件 * * 返回的超時為 -1,表示它不介意 poll 呼叫阻塞多長時間*/gboolean g_source_echo_prepare(GSource * source, gint * timeout){ *timeout = -1; return FALSE;}/* 測試 poll 呼叫的結果,以檢視事件是否發生 */gboolean g_source_echo_check(GSource * source){ GSourceEcho *source_echo = (GSourceEcho *) source; /* events 為要監聽的事件 * revents 為 poll 監聽到的事件 * * 因為只監聽了一個事件,因此簡化了比較方式 */ if (source_echo->fd.revents != source_echo->fd.events) { return FALSE; } return TRUE;}gboolean g_source_echo_dispatch(GSource * source, GSourceFunc callback, gpointer user_data){ gboolean again = G_SOURCE_REMOVE; GSourceEcho *source_echo = (GSourceEcho *) source; if (callback) { again = callback(source_echo->channel); } return again;}/* 釋放 source 持有的資源的應用計數 * 然後 source 才可以被安全地銷燬,不會造成記憶體洩露 */void g_source_echo_finalize(GSource * source){ GSourceEcho *source_echo = (GSourceEcho *) source; if (source_echo->channel) { g_io_channel_unref(source_echo->channel); }}gboolean echo(GIOChannel * channel){ gsize len = 0; gchar *buf = NULL; g_io_channel_read_line(channel, &buf, &len, NULL, NULL); if (len > 0) { g_print("%s", buf); g_io_channel_write_chars(channel, buf, len, NULL, NULL); g_io_channel_flush(channel, NULL); } g_free(buf); return TRUE;}int client(void){ struct sockaddr_in serv_addr; if (-1 == (client_fd = socket(AF_INET, SOCK_STREAM, 0))) { return -1; } memset(&serv_addr, 0, sizeof serv_addr); serv_addr.sin_family = AF_INET; serv_addr.sin_addr.s_addr = inet_addr("127.0.0.1"); serv_addr.sin_port = htons(9999); if (-1 == connect(client_fd, (struct sockaddr *) &serv_addr, sizeof serv_addr)) { return -1; } return 0;}
在TCP Server的控制檯上輸入字串,示例即可輸出該字串,TCP Server能夠收到示例的響應。