天天看點

【原創】通過 ioctl + FIONREAD 判定資料可讀

在排查業務 bug 的過程中,看到如下兩種輸出資訊: 

tcp 連接配接正常情況下,進行資料讀取 

tcp 連接配接斷開情況下(人為強制斷開),進行資料讀取 

兩種情況的對比圖如下(為了顯示友善,右側圖中多餘資料已删除) 

【原創】通過 ioctl + FIONREAD 判定資料可讀

由圖中所示,可以得到如下結論: 

ioctl 能夠判定 socket 緩沖區中可讀資料的數量,兩種情況下 ioctl 調用都傳回 0 ,即成功。

readv 在兩種情況都進行了資料讀取,tcp 鍊路正常情況下,readv 傳回讀取的資料位元組數;tcp 鍊路異常情況下,readv 傳回 0 。

libevent 中的對應代碼實作如下: 

【原創】通過 ioctl + FIONREAD 判定資料可讀

按理說,這個問題到這裡應該算結束了,但是不幸的是,我又搜到了下面這個文章: 

=== 我是琅琊榜的分隔線 === 

問題: 

i'm trying to get to know how many bytes there are readable at my tcp socket. i am calling ioctl with the flag "fionread" which should actually give me this value. when i call the function i get as return val 0 ( so no error ) but also my integer argument gets the value 0. that would be no problem but when i call the recv() method i actually read some bytes out of the socket. what am i doing wrong? 

嘗試擷取 tcp socket 上有多少位元組資料可讀,調用使用 ioctl 配合 fionread 進行擷取。當調用 ioctl 後得到傳回值 0 表明沒有錯誤發生,但是儲存目前可讀位元組數量的變量内容同樣為 0 。這種情況資料正常行為,但是當代碼中實際執行 recv() 調用時,發現能夠從目前 socket 上讀出若幹位元組來。哪裡有問題呢? 

when i call the recv function i acutally read some valid data ( which i expected ) 

有人回答如下: 

the real answer here is to use select(2) like cnicutar said. toby, what you aren't understanding is that you have a race condition. first you look at the socket and ask how many bytes are there. then, while your code is processing the "no data here" block, bytes are being received by the hardware & os asynchronous to your application. so, by the time that the recv() function is called, the answer of "no bytes are available" is no longer true... 

問題的真正原因是,上述代碼調用存在競争問題。首先,你通過調用 ioctl 檢視 socket 中是否存在可讀位元組,再你檢視的那該時刻,可能恰好确實沒有資料可讀;而之後,在代碼進行“無資料存在”邏輯處理時,可能已經經由硬體和作業系統異步的向應用程式傳輸資料了。是以,當代碼執行到 recv() 函數時,“無資料存在”這個結論可能已經不正确了。 

sure, a small sleep probably fixed your program two years ago, but it also taught you terrible coding practice and you lost out on an opportunity to learn how to use sockets correctly by using select(). 

通過調用 sleep 進行短暫的休眠可以簡單粗暴的修複次問題,但是這絕非正确的程式設計實踐,正确的作為應該是基于 select 進行處理。 

further, as karoly horvath said, you can tell recv to not read more bytes than you can store in the buffer that the user passed in. then your function interface becomes "this fn will return as many bytes as are available on the socket, but not more than [buffer size you passed in]". 

更進一步,你可以令 recv() 至多讀取指定 buffer 長度的資料,故函數接口行為就變成了“該函數将傳回目前 socket 上可讀盡可能多的資料,但是不超過傳入 buffer 的長度”。 

this means that this function doesn't need to worry about clearing the buffer any more. the caller can call your function as many times as necessary to clear all of the bytes out of it (or you can provide a separate fn that discards the data wholesale and not tie up that functionality in any specific data gather function). your function is more flexible by not doing too many things. you can then create a wrapper function that is smart to your data transfer needs of a particular application, and that fn calls the get_data fn and the clear_socket fn as needed for that specific app. now you are building a library you can carry around from project to project, and maybe job to job if you're so lucky as to have an employer that lets you take code with you. 

這就意味着,該函數将不再需要擔心 buffer 内容清除的問題。調用者可以任意次數調用該函數,因為其自帶資料清除效果(或者你可以提供單獨的函數進行資料清除動作,而不将此功能綁定到任何特定的資料擷取函數上)。你的函數将更加靈活,因為其不用負責過多工作。你可以建立一個更符合你要的封裝函數,以針對特定應用中資料傳輸的需要,而該封裝函數内部會調用 get_data 和 clear_socket 函數進行相應處理。通過這種方式,你就可以得到一個可以在各種工程中随意使用的庫了。 

=== 我是琅琊榜的分隔線 ===

      乍一看,似乎文章中描述的情況和上面 tcp 鍊路斷開時的情況類似,但事實上是不同的,關鍵在于 readv 的傳回值為 0 ,至于 readv 第二個參數 const struct iovec *iov 的内容,實際為 iov_type vecs[0] 所指向記憶體位址中的内容,并非從 socket 上讀取的内容。 至于 libevent 中使用 ioctl 時是否存在 race condition 問題,其實多慮了,提供代碼片段如下:

【原創】通過 ioctl + FIONREAD 判定資料可讀
【原創】通過 ioctl + FIONREAD 判定資料可讀
【原創】通過 ioctl + FIONREAD 判定資料可讀

繼續閱讀