glib學習筆記之三——GLib核心應用支援:glib 中 IO Channels 理解
iochannel可以把開發者指定的發生在 檔案描述符、管道和socket之上的事件轉換為glib的內部事件,從而可以在程式中用統一的方法來處理IO事件和使用者互動。
iochannel支援的IO事件有 可讀、可寫、有緊急(urgent)資料到達、出錯、結束通話。由於iochannel是在 檔案描述符、管道和socket 的基礎上構建的,所以它提供的方法既包括這三者的共同點,也考慮到了這三者的不同之處。
那麼怎樣在基於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)外的其他事件都建立回撥函式,加入事件迴圈。當有資料來時被動讀取,傳送資料時主動傳送。
一個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讀取函式讀。
不過要注意的是必須要用迴圈讀到出現沒有資料可讀以致返回錯誤時才能結束一次讀操作。這是因為核心中有緩衝,要是一次讀取沒有把全部資料讀完的話,本應該
在這次回撥中讀取的資料就要等到下一次才能讀取了。串列埠的資料流量不大,不用這種處理辦法可能也不會有問題,不過還是保險一點好。上面開啟串列埠時使用非阻
塞方式就是為了這裡可以把達到的資料完整讀完。
5. 當停止讀取的事件發生時,回撥函式應該做如下工作:
* (1). 退出事件迴圈: g_source_remove (source_id);
* (2). 關閉 IO_Channel: g_io_channel_shutdown (io_channel, TRUE, NULL);
* (3). 釋放 IO_Channel: g_io_channel_unref (io_channel);
關閉 iochannel 操作會把檔案描述符關閉。
6. 其他:
開啟和關閉 iochannel 的順序不可變。
要是隻想暫時不讀檔案妙算符,可以只退出事件迴圈。
不保證退出事件迴圈後到來的資料是否會在核心中快取,不保證這段時間內的資料是否全部被快取,所以當你退出事件迴圈再加入時要自己檢查資料是否是你所需要的。(不保證是因為我沒有做過試驗)
iochannel不是什麼新技術,它的基礎是 select / poll,對比一下 g_io_add_watch 提供的事件選項和
select / poll 提供的就清楚了。關於 select / poll 請 man 2 select_tut或者 man 2 poll。
上面提到的方法對其他檔案描述符都適用,我之前是把它用在socket上。