1. 程式人生 > >圖解 | 當我們在讀寫 Socket 時,我們究竟在讀寫什麼?

圖解 | 當我們在讀寫 Socket 時,我們究竟在讀寫什麼?

套接字socket是大多數程式設計師都非常熟悉的概念,它是計算機網路程式設計的基礎,TCP/UDP收發訊息都靠它。我們熟悉的web伺服器底層依賴它,我們用到的MySQL關係資料庫、Redis記憶體資料庫底層依賴它。我們用微信和別人聊天也依賴它,我們玩網路遊戲時依賴它,讀者們能夠閱讀這篇文章也是因為有它在背後默默地支援著網路通訊。

簡單過程

當客戶端和伺服器使用TCP協議進行通訊時,客戶端封裝一個請求物件req,將請求物件req序列化成位元組陣列,然後通過套接字socket將位元組陣列傳送到伺服器,伺服器通過套接字socket讀取到位元組陣列,再反序列化成請求物件req,進行處理,處理完畢後,生成一個響應對應res,將響應物件res序列化成位元組陣列,然後通過套接字將自己陣列傳送給客戶端,客戶端通過套接字socket讀取到自己陣列,再反序列化成響應物件。

通訊框架往往可以將序列化的過程隱藏起來,我們所看到的現象就是上圖所示,請求物件req和響應物件res在客戶端和伺服器之間跑來跑去。

也許你覺得這個過程還是挺簡單的,很好理解,但是實際上背後發生的一系列事件超出了你們中大多數人的想象。通訊的真實過程要比上面的這張圖複雜太多。你也許會問,我們需要了解的那麼深入麼,直接拿來用不就可以了麼?

在網際網路技術服務行業工作多年的經驗告訴我,如果你對底層機制不瞭解,你就會不明白為什麼對套接字socket的讀寫會出現各種奇奇乖乖的問題,為什麼有時會阻塞,有時又不阻塞,有時候還報錯,為什麼會有粘包半包問題,NIO具體又是什麼,它是什麼特別新鮮的技術麼?對於這些問題的理解都需要你瞭解底層機制。

細節過程

為了方便大家對通訊底層的理解,我花了些時間做了下面這個動畫,它並不能完全覆蓋底層細節的全貌,但是對於理解套接字的工作機制已經足夠了。請讀者仔細觀察這個動畫,後面的講解將圍繞著這個動畫展開。

我們平時用到的套接字其實只是一個引用(一個物件ID),這個套接字物件實際上是放在作業系統核心中。這個套接字物件內部有兩個重要的緩衝結構,一個是讀緩衝(read buffer),一個是寫緩衝(write buffer),它們都是有限大小的陣列結構。

當我們對客戶端的socket寫入位元組陣列時(序列化後的請求訊息物件req),是將位元組陣列拷貝到核心區套接字物件的write buffer中,核心網路模組會有單獨的執行緒負責不停地將write buffer的資料拷貝到網絡卡硬體,網絡卡硬體再將資料送到網線,經過一些列路由器交換機,最終送達伺服器的網絡卡硬體中。

同樣,伺服器核心的網路模組也會有單獨的執行緒不停地將收到的資料拷貝到套接字的read buffer中等待使用者層來讀取。最終伺服器的使用者程序通過socket引用的read方法將read buffer中的資料拷貝到使用者程式記憶體中進行反序列化成請求物件進行處理。然後伺服器將處理後的響應物件走一個相反的流程傳送給客戶端,這裡就不再具體描述。

阻塞

我們注意到write buffer空間都是有限的,所以如果應用程式往套接字裡寫的太快,這個空間是會滿的。一旦滿了,寫操作就會阻塞,直到這個空間有足夠的位置騰出來。不過有了NIO(非阻塞IO),寫操作也可以不阻塞,能寫多少是多少,通過返回值來確定到底寫進去多少,那些沒有寫進去的內容使用者程式會快取起來,後續會繼續重試寫入。

同樣我們也注意到read buffer的內容可能會是空的。這樣套接字的讀操作(一般是讀一個定長的位元組陣列)也會阻塞,直到read buffer中有了足夠的內容(填充滿位元組陣列)才會返回。有了NIO,就可以有多少讀多少,無須阻塞了。讀不夠的,後續會繼續嘗試讀取。

ack

那上面這張圖就展現了套接字的全部過程麼?顯然不是,資料的確認過程(ack)就完全沒有展現。比如當寫緩衝的內容拷貝到網絡卡後,是不會立即從寫緩衝中將這些拷貝的內容移除的,而要等待對方的ack過來之後才會移除。如果網路狀況不好,ack遲遲不過來,寫緩衝很快就會滿的。

包頭

細心的同學可能注意到圖中的訊息req被拷貝到網絡卡的時候變成了大寫的REQ,這是為什麼呢?因為這兩個東西已經不是完全一樣的了。核心的網路模組會將緩衝區的訊息進行分塊傳輸,如果緩衝區的內容太大,是會被拆分成多個獨立的小訊息包的。並且還要在每個訊息包上附加上一些額外的頭資訊,比如源網絡卡地址和目標網絡卡地址、訊息的序號等資訊,到了接收端需要對這些訊息包進行重新排序組裝去頭後才會扔進讀緩衝中。這些複雜的細節過程就非常難以在動畫上予以呈現了。

速率

還有個問題那就是如果讀緩衝滿了怎麼辦,網絡卡收到了對方的訊息要怎麼處理?一般的做法就是丟棄掉不給對方ack,對方如果發現ack遲遲沒有來,就會重發訊息。那緩衝為什麼會滿?是因為訊息接收方處理的慢而傳送方生產的訊息太快了,這時候tcp協議就會有個動態視窗調整演算法來限制傳送方的傳送速率,使得收發效率趨於匹配。如果是udp協議的話,訊息一丟那就徹底丟了。