使用gtk+的iochannel進行事件驅動IO操作
阿新 • • 發佈:2019-02-02
現代的GUI系統都是基於事件驅動的,其中必有一個事件迴圈過程來獲取和處理事件。gtk也一樣,gtk的事件迴圈過程是由glib提供的,而iochannel是glib中把IO事件整合到事件的一種手段。
iochannel可以把開發者指定的發生在 檔案描述符、管道和socket之上的事件轉換為glib的內部事件,從而可以在程式中用統一的方法來處理IO事件和使用者互動。
iochannel支援的IO事件有 可讀、可寫、有緊急(urgent)資料到達、出錯、結束通話。由於iochannel是在 檔案描述符、管道和socket 的基礎上構建的,所以它提供的方法既包括這三者的共同點,也考慮到了這三者的不同之處。
另外,iochannel還提供了讀寫 檔案描述符、管道和socket的方法。
可以把iochannel看作是glib對這三者的一個抽象。
上面簡單介紹了iochannel,(水平不高,說來說去也就幾句話:-p)
那麼怎樣在基於gtk或者glib的程式中加入iochannel呢?
步驟一般為:
1. 建立一個檔案描述符。
可以通過開啟一個普通檔案、建立管道或者開啟socket來實現,結果都是得到一個檔案描述符。
2. 建立iochannel,設定資料編碼。
iochannel通過如下函式建立:
GIOChannel* g_io_channel_unix_new (int fd);
例如:
io_channel = g_io_channel_unix_new (fd);
建立之後可以設定資料編碼。對於資料編碼,我也不太明,一般我把編碼設定為NULL,這樣在使用iochannel提供的讀寫函式時就不會對資料進行任何處理。編碼設定函式如下:
GIOStatus g_io_channel_set_encoding (GIOChannel *channel, const gchar *encoding, GError **error);
例如,把編碼設定為NULL:
g_io_channel_set_encoding (io_channel, NULL, &err);
3. 把你所需要處理的發生檔案描述符上的事件加到事件迴圈中。
通過如下函式把iochannel的指定事件加入到事件迴圈中:
guint g_io_add_watch (GIOChannel *channel, GIOCondition condition, GIOFunc func, gpointer user_data);
其中,GIOCondition包括G_IO_IN, G_IO_OUT, G_IO_PRI,G_IO_ERR, G_IO_HUP, G_IO_NVAL。可以通過對它們的或運算來同時指定多個事件,當然回撥函式應該判斷是哪個的事件引起回撥。
iochannel的回撥函式原型為:
gboolean (*GIOFunc) (GIOChannel *source, GIOCondition condition, gpointer data);
第二個引數便是引起回撥的事件的值。
上面第1步的作用就好比建立一個按鈕,而第2,3步的作用就好比用g_signal_connect()把一個事件加入事件迴圈。做好這3個工作,後面還有兩個工作:
1. 編寫回調函式。
在回撥函式中,你可以採用iochannel提供的讀寫函式,也可以用g_io_channel_unix_get_fd()獲得的檔案描述符來進行平常的IO操作。
2. 退出事件迴圈,關閉iochannel。
在程式結束,或者檔案描述符已經沒用的時候,應該關閉iochannel。在關閉前必須先退出事件迴圈,用g_source_remove(source_id)完成退出動作。source_id是g_io_add_watch()的返回值。
跟著便可以關閉iochannel了,用g_io_channel_shutdown (io_channel, TRUE, NULL)來完成關閉動作。
關閉後iochannel所佔記憶體還沒有釋放,用g_io_channel_unref (io_channel)來減少iochannel的參考計數器,使其為0,glib會自動釋放該iochannel。
根據你的應用,我的建議是在連線到伺服器之後利用socket的檔案描述符建立iochannel,並且為除 資料可寫(G_IO_OUT)外的其他事件都建立回撥函式,加入事件迴圈。當有資料來時被動讀取,傳送資料時主動傳送。
我寫gtk程式,一般是先用glade畫出介面,生成程式碼,然後提取程式碼,對程式碼進行修改,讓程式碼符合我的要求和軟體設計的要求,然後編寫回調函式。
要想不寫圖形介面方面的程式碼那是很難的,我的所謂的整合開發環境就是glade+emacs,然後glib, gdk, gtk的參考手冊是我最常查閱的,函式太多,記不住,pango的參考手冊也用過,但是比較少,atk的好象沒用到過。
上面有關iochannel的其實也就之前我的帖子的另一個表述,沒什麼新的東西。
對進入自己不熟悉的領域,我的意見是多看文件、參考一下簡單的程式碼,然後自己多實踐。關鍵是弄清楚其來龍去脈,計算機的東西不會無中生有,理順了就明白了。
一個iochannel只能繫結一個socket。
伺服器那端,用listen之後的fd (設為listen_fd) 建立一個iochannel。
當有連線來時,iochannel表現為listen_fd可讀。在回撥函式中accept,得到一個連線fd(設為connect_fd)。然後為每一個connect_fd建立一個iochannel。
簡單的說就是listen_fd的回撥函式是accept用的;connect_fd的回撥函式是讀寫用的,每個connect_fd的回撥函式都一樣。
connect_fd關閉後關閉該iochannel。
關於關閉iochannel,可能要用g_idle_add()新增一個垃圾回收函式。
因為不能在connect_fd的回撥函式中shutdown該iochannel。
客戶端要注意connect的超時時間比較長,可能需要用到執行緒來解決這個問題。
想實現“在按鍵事件開始後不斷的讀串列埠,直到關斷串列埠的按鍵事件啟動”的話,用while是不可行的,單單加入非阻塞也不行,因為在while迴圈中你的程式將不會響應按鍵事件。
多執行緒是可以解決問題的,不過儘量不要使用。
用iochannel是最合適的。方法大致如下:
1. 以非阻塞方式開啟串列埠,非阻塞是必須的,下面會提到原因。
fd = open("/dev/ttyS0",O_RDWR|O_NONBLOCK,0644);
2. 建立iochannel。
io_channel = g_io_channel_unix_new (fd);
g_io_channel_set_encoding (io_channel, NULL, &err); /* 應該可選 */
3. 把檔案描述符可讀的事件加入到程式的事件迴圈中:
source_id = g_io_add_watch (io_channel, G_IO_IN, read_ttyS, NULL);
4. 當這些做好後就可以用 read_ttyS() 來讀取串列埠資料了。
你可以用 read() 來直接讀串列埠資料,也可以用 glib 提供的iochannel讀取函式讀。
不過要注意的是必須要用迴圈讀到出現沒有資料可讀以致返回錯誤時才能結束一次讀操作。這是因為核心中有緩衝,要是一次讀取沒有把全部資料讀完的話,本應該 在這次回撥中讀取的資料就要等到下一次才能讀取了。串列埠的資料流量不大,不用這種處理辦法可能也不會有問題,不過還是保險一點好。上面開啟串列埠時使用非阻 塞方式就是為了這裡可以把達到的資料完整讀完。
iochannel可以把開發者指定的發生在 檔案描述符、管道和socket之上的事件轉換為glib的內部事件,從而可以在程式中用統一的方法來處理IO事件和使用者互動。
iochannel支援的IO事件有 可讀、可寫、有緊急(urgent)資料到達、出錯、結束通話。由於iochannel是在 檔案描述符、管道和socket 的基礎上構建的,所以它提供的方法既包括這三者的共同點,也考慮到了這三者的不同之處。
另外,iochannel還提供了讀寫 檔案描述符、管道和socket的方法。
可以把iochannel看作是glib對這三者的一個抽象。
上面簡單介紹了iochannel,(水平不高,說來說去也就幾句話:-p)
那麼怎樣在基於gtk或者glib的程式中加入iochannel呢?
步驟一般為:
1. 建立一個檔案描述符。
可以通過開啟一個普通檔案、建立管道或者開啟socket來實現,結果都是得到一個檔案描述符。
2. 建立iochannel,設定資料編碼。
iochannel通過如下函式建立:
GIOChannel* g_io_channel_unix_new (int fd);
例如:
io_channel = g_io_channel_unix_new (fd);
建立之後可以設定資料編碼。對於資料編碼,我也不太明,一般我把編碼設定為NULL,這樣在使用iochannel提供的讀寫函式時就不會對資料進行任何處理。編碼設定函式如下:
GIOStatus g_io_channel_set_encoding (GIOChannel *channel, const gchar *encoding, GError **error);
例如,把編碼設定為NULL:
g_io_channel_set_encoding (io_channel, NULL, &err);
3. 把你所需要處理的發生檔案描述符上的事件加到事件迴圈中。
通過如下函式把iochannel的指定事件加入到事件迴圈中:
guint g_io_add_watch (GIOChannel *channel, GIOCondition condition, GIOFunc func, gpointer user_data);
其中,GIOCondition包括G_IO_IN, G_IO_OUT, G_IO_PRI,G_IO_ERR, G_IO_HUP, G_IO_NVAL。可以通過對它們的或運算來同時指定多個事件,當然回撥函式應該判斷是哪個的事件引起回撥。
iochannel的回撥函式原型為:
gboolean (*GIOFunc) (GIOChannel *source, GIOCondition condition, gpointer data);
第二個引數便是引起回撥的事件的值。
上面第1步的作用就好比建立一個按鈕,而第2,3步的作用就好比用g_signal_connect()把一個事件加入事件迴圈。做好這3個工作,後面還有兩個工作:
1. 編寫回調函式。
在回撥函式中,你可以採用iochannel提供的讀寫函式,也可以用g_io_channel_unix_get_fd()獲得的檔案描述符來進行平常的IO操作。
2. 退出事件迴圈,關閉iochannel。
在程式結束,或者檔案描述符已經沒用的時候,應該關閉iochannel。在關閉前必須先退出事件迴圈,用g_source_remove(source_id)完成退出動作。source_id是g_io_add_watch()的返回值。
跟著便可以關閉iochannel了,用g_io_channel_shutdown (io_channel, TRUE, NULL)來完成關閉動作。
關閉後iochannel所佔記憶體還沒有釋放,用g_io_channel_unref (io_channel)來減少iochannel的參考計數器,使其為0,glib會自動釋放該iochannel。
根據你的應用,我的建議是在連線到伺服器之後利用socket的檔案描述符建立iochannel,並且為除 資料可寫(G_IO_OUT)外的其他事件都建立回撥函式,加入事件迴圈。當有資料來時被動讀取,傳送資料時主動傳送。
我寫gtk程式,一般是先用glade畫出介面,生成程式碼,然後提取程式碼,對程式碼進行修改,讓程式碼符合我的要求和軟體設計的要求,然後編寫回調函式。
要想不寫圖形介面方面的程式碼那是很難的,我的所謂的整合開發環境就是glade+emacs,然後glib, gdk, gtk的參考手冊是我最常查閱的,函式太多,記不住,pango的參考手冊也用過,但是比較少,atk的好象沒用到過。
上面有關iochannel的其實也就之前我的帖子的另一個表述,沒什麼新的東西。
對進入自己不熟悉的領域,我的意見是多看文件、參考一下簡單的程式碼,然後自己多實踐。關鍵是弄清楚其來龍去脈,計算機的東西不會無中生有,理順了就明白了。
一個iochannel只能繫結一個socket。
伺服器那端,用listen之後的fd (設為listen_fd) 建立一個iochannel。
當有連線來時,iochannel表現為listen_fd可讀。在回撥函式中accept,得到一個連線fd(設為connect_fd)。然後為每一個connect_fd建立一個iochannel。
簡單的說就是listen_fd的回撥函式是accept用的;connect_fd的回撥函式是讀寫用的,每個connect_fd的回撥函式都一樣。
connect_fd關閉後關閉該iochannel。
關於關閉iochannel,可能要用g_idle_add()新增一個垃圾回收函式。
因為不能在connect_fd的回撥函式中shutdown該iochannel。
客戶端要注意connect的超時時間比較長,可能需要用到執行緒來解決這個問題。
想實現“在按鍵事件開始後不斷的讀串列埠,直到關斷串列埠的按鍵事件啟動”的話,用while是不可行的,單單加入非阻塞也不行,因為在while迴圈中你的程式將不會響應按鍵事件。
多執行緒是可以解決問題的,不過儘量不要使用。
用iochannel是最合適的。方法大致如下:
1. 以非阻塞方式開啟串列埠,非阻塞是必須的,下面會提到原因。
fd = open("/dev/ttyS0",O_RDWR|O_NONBLOCK,0644);
2. 建立iochannel。
io_channel = g_io_channel_unix_new (fd);
g_io_channel_set_encoding (io_channel, NULL, &err); /* 應該可選 */
3. 把檔案描述符可讀的事件加入到程式的事件迴圈中:
source_id = g_io_add_watch (io_channel, G_IO_IN, read_ttyS, NULL);
4. 當這些做好後就可以用 read_ttyS() 來讀取串列埠資料了。
你可以用 read() 來直接讀串列埠資料,也可以用 glib 提供的iochannel讀取函式讀。
不過要注意的是必須要用迴圈讀到出現沒有資料可讀以致返回錯誤時才能結束一次讀操作。這是因為核心中有緩衝,要是一次讀取沒有把全部資料讀完的話,本應該 在這次回撥中讀取的資料就要等到下一次才能讀取了。串列埠的資料流量不大,不用這種處理辦法可能也不會有問題,不過還是保險一點好。上面開啟串列埠時使用非阻 塞方式就是為了這裡可以把達到的資料完整讀完。