作者:老錢
鏈接:https://juejin.im/post/5b344ad6e51d4558892eeb46

套接字socket是大多數程序員都非常熟悉的概念,它是計算機網絡編程的基礎,TCP/UDP收發消息都靠它。我們熟悉的web服務器底層依賴它,我們用到的MySQL關係數據庫、Redis內存數據庫底層依賴它。我們用微信和別人聊天也依賴它,我們玩網絡遊戲時依賴它,讀者們能夠閱讀這篇文章也是因爲有它在背後默默地支持着網絡通信。

簡單過程

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

「動畫」當我們在讀寫Socket時,我們究竟在讀寫什麼

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

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

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

細節過程

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

「動畫」當我們在讀寫Socket時,我們究竟在讀寫什麼

我們平時用到的套接字其實只是一個引用(一個對象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協議的話,消息一丟那就徹底丟了。

相关文章