图解 | 当我们在读写 Socket 时,我们究竟在读写什么?
套接字socket是大多數(shù)程序員都非常熟悉的概念,它是計(jì)算機(jī)網(wǎng)絡(luò)編程的基礎(chǔ),TCP/UDP收發(fā)消息都靠它。我們熟悉的web服務(wù)器底層依賴(lài)它,我們用到的MySQL關(guān)系數(shù)據(jù)庫(kù)、Redis內(nèi)存數(shù)據(jù)庫(kù)底層依賴(lài)它。我們用微信和別人聊天也依賴(lài)它,我們玩網(wǎng)絡(luò)游戲時(shí)依賴(lài)它,讀者們能夠閱讀這篇文章也是因?yàn)橛兴诒澈竽刂С种W(wǎng)絡(luò)通信。
簡(jiǎn)單過(guò)程
當(dāng)客戶(hù)端和服務(wù)器使用TCP協(xié)議進(jìn)行通信時(shí),客戶(hù)端封裝一個(gè)請(qǐng)求對(duì)象req,將請(qǐng)求對(duì)象req序列化成字節(jié)數(shù)組,然后通過(guò)套接字socket將字節(jié)數(shù)組發(fā)送到服務(wù)器,服務(wù)器通過(guò)套接字socket讀取到字節(jié)數(shù)組,再反序列化成請(qǐng)求對(duì)象req,進(jìn)行處理,處理完畢后,生成一個(gè)響應(yīng)對(duì)應(yīng)res,將響應(yīng)對(duì)象res序列化成字節(jié)數(shù)組,然后通過(guò)套接字將自己數(shù)組發(fā)送給客戶(hù)端,客戶(hù)端通過(guò)套接字socket讀取到自己數(shù)組,再反序列化成響應(yīng)對(duì)象。
通信框架往往可以將序列化的過(guò)程隱藏起來(lái),我們所看到的現(xiàn)象就是上圖所示,請(qǐng)求對(duì)象req和響應(yīng)對(duì)象res在客戶(hù)端和服務(wù)器之間跑來(lái)跑去。
也許你覺(jué)得這個(gè)過(guò)程還是挺簡(jiǎn)單的,很好理解,但是實(shí)際上背后發(fā)生的一系列事件超出了你們中大多數(shù)人的想象。通信的真實(shí)過(guò)程要比上面的這張圖復(fù)雜太多。你也許會(huì)問(wèn),我們需要了解的那么深入么,直接拿來(lái)用不就可以了么?
在互聯(lián)網(wǎng)技術(shù)服務(wù)行業(yè)工作多年的經(jīng)驗(yàn)告訴我,如果你對(duì)底層機(jī)制不了解,你就會(huì)不明白為什么對(duì)套接字socket的讀寫(xiě)會(huì)出現(xiàn)各種奇奇乖乖的問(wèn)題,為什么有時(shí)會(huì)阻塞,有時(shí)又不阻塞,有時(shí)候還報(bào)錯(cuò),為什么會(huì)有粘包半包問(wèn)題,NIO具體又是什么,它是什么特別新鮮的技術(shù)么?對(duì)于這些問(wèn)題的理解都需要你了解底層機(jī)制。
細(xì)節(jié)過(guò)程
為了方便大家對(duì)通信底層的理解,我花了些時(shí)間做了下面這個(gè)動(dòng)畫(huà),它并不能完全覆蓋底層細(xì)節(jié)的全貌,但是對(duì)于理解套接字的工作機(jī)制已經(jīng)足夠了。請(qǐng)讀者仔細(xì)觀察這個(gè)動(dòng)畫(huà),后面的講解將圍繞著這個(gè)動(dòng)畫(huà)展開(kāi)。
我們平時(shí)用到的套接字其實(shí)只是一個(gè)引用(一個(gè)對(duì)象ID),這個(gè)套接字對(duì)象實(shí)際上是放在操作系統(tǒng)內(nèi)核中。這個(gè)套接字對(duì)象內(nèi)部有兩個(gè)重要的緩沖結(jié)構(gòu),一個(gè)是讀緩沖(read buffer),一個(gè)是寫(xiě)緩沖(write buffer),它們都是有限大小的數(shù)組結(jié)構(gòu)。
當(dāng)我們對(duì)客戶(hù)端的socket寫(xiě)入字節(jié)數(shù)組時(shí)(序列化后的請(qǐng)求消息對(duì)象req),是將字節(jié)數(shù)組拷貝到內(nèi)核區(qū)套接字對(duì)象的write buffer中,內(nèi)核網(wǎng)絡(luò)模塊會(huì)有單獨(dú)的線(xiàn)程負(fù)責(zé)不停地將write buffer的數(shù)據(jù)拷貝到網(wǎng)卡硬件,網(wǎng)卡硬件再將數(shù)據(jù)送到網(wǎng)線(xiàn),經(jīng)過(guò)一些列路由器交換機(jī),最終送達(dá)服務(wù)器的網(wǎng)卡硬件中。
同樣,服務(wù)器內(nèi)核的網(wǎng)絡(luò)模塊也會(huì)有單獨(dú)的線(xiàn)程不停地將收到的數(shù)據(jù)拷貝到套接字的read buffer中等待用戶(hù)層來(lái)讀取。最終服務(wù)器的用戶(hù)進(jìn)程通過(guò)socket引用的read方法將read buffer中的數(shù)據(jù)拷貝到用戶(hù)程序內(nèi)存中進(jìn)行反序列化成請(qǐng)求對(duì)象進(jìn)行處理。然后服務(wù)器將處理后的響應(yīng)對(duì)象走一個(gè)相反的流程發(fā)送給客戶(hù)端,這里就不再具體描述。
阻塞
我們注意到write buffer空間都是有限的,所以如果應(yīng)用程序往套接字里寫(xiě)的太快,這個(gè)空間是會(huì)滿(mǎn)的。一旦滿(mǎn)了,寫(xiě)操作就會(huì)阻塞,直到這個(gè)空間有足夠的位置騰出來(lái)。不過(guò)有了NIO(非阻塞IO),寫(xiě)操作也可以不阻塞,能寫(xiě)多少是多少,通過(guò)返回值來(lái)確定到底寫(xiě)進(jìn)去多少,那些沒(méi)有寫(xiě)進(jìn)去的內(nèi)容用戶(hù)程序會(huì)緩存起來(lái),后續(xù)會(huì)繼續(xù)重試寫(xiě)入。
同樣我們也注意到read buffer的內(nèi)容可能會(huì)是空的。這樣套接字的讀操作(一般是讀一個(gè)定長(zhǎng)的字節(jié)數(shù)組)也會(huì)阻塞,直到read buffer中有了足夠的內(nèi)容(填充滿(mǎn)字節(jié)數(shù)組)才會(huì)返回。有了NIO,就可以有多少讀多少,無(wú)須阻塞了。讀不夠的,后續(xù)會(huì)繼續(xù)嘗試讀取。
ack
那上面這張圖就展現(xiàn)了套接字的全部過(guò)程么?顯然不是,數(shù)據(jù)的確認(rèn)過(guò)程(ack)就完全沒(méi)有展現(xiàn)。比如當(dāng)寫(xiě)緩沖的內(nèi)容拷貝到網(wǎng)卡后,是不會(huì)立即從寫(xiě)緩沖中將這些拷貝的內(nèi)容移除的,而要等待對(duì)方的ack過(guò)來(lái)之后才會(huì)移除。如果網(wǎng)絡(luò)狀況不好,ack遲遲不過(guò)來(lái),寫(xiě)緩沖很快就會(huì)滿(mǎn)的。
包頭
細(xì)心的同學(xué)可能注意到圖中的消息req被拷貝到網(wǎng)卡的時(shí)候變成了大寫(xiě)的REQ,這是為什么呢?因?yàn)檫@兩個(gè)東西已經(jīng)不是完全一樣的了。內(nèi)核的網(wǎng)絡(luò)模塊會(huì)將緩沖區(qū)的消息進(jìn)行分塊傳輸,如果緩沖區(qū)的內(nèi)容太大,是會(huì)被拆分成多個(gè)獨(dú)立的小消息包的。并且還要在每個(gè)消息包上附加上一些額外的頭信息,比如源網(wǎng)卡地址和目標(biāo)網(wǎng)卡地址、消息的序號(hào)等信息,到了接收端需要對(duì)這些消息包進(jìn)行重新排序組裝去頭后才會(huì)扔進(jìn)讀緩沖中。這些復(fù)雜的細(xì)節(jié)過(guò)程就非常難以在動(dòng)畫(huà)上予以呈現(xiàn)了。
速率
還有個(gè)問(wèn)題那就是如果讀緩沖滿(mǎn)了怎么辦,網(wǎng)卡收到了對(duì)方的消息要怎么處理?一般的做法就是丟棄掉不給對(duì)方ack,對(duì)方如果發(fā)現(xiàn)ack遲遲沒(méi)有來(lái),就會(huì)重發(fā)消息。那緩沖為什么會(huì)滿(mǎn)?是因?yàn)橄⒔邮辗教幚淼穆l(fā)送方生產(chǎn)的消息太快了,這時(shí)候tcp協(xié)議就會(huì)有個(gè)動(dòng)態(tài)窗口調(diào)整算法來(lái)限制發(fā)送方的發(fā)送速率,使得收發(fā)效率趨于匹配。如果是udp協(xié)議的話(huà),消息一丟那就徹底丟了。
總結(jié)
以上是生活随笔為你收集整理的图解 | 当我们在读写 Socket 时,我们究竟在读写什么?的全部?jī)?nèi)容,希望文章能夠幫你解決所遇到的問(wèn)題。
- 上一篇: MySQL 中一个双引号的错位引发的血案
- 下一篇: 为什么大数据需要数据湖?