日韩性视频-久久久蜜桃-www中文字幕-在线中文字幕av-亚洲欧美一区二区三区四区-撸久久-香蕉视频一区-久久无码精品丰满人妻-国产高潮av-激情福利社-日韩av网址大全-国产精品久久999-日本五十路在线-性欧美在线-久久99精品波多结衣一区-男女午夜免费视频-黑人极品ⅴideos精品欧美棵-人人妻人人澡人人爽精品欧美一区-日韩一区在线看-欧美a级在线免费观看

歡迎訪問 生活随笔!

生活随笔

當前位置: 首頁 > 编程资源 > 编程问答 >内容正文

编程问答

TCP之深入浅出send和recv

發(fā)布時間:2025/3/21 编程问答 30 豆豆
生活随笔 收集整理的這篇文章主要介紹了 TCP之深入浅出send和recv 小編覺得挺不錯的,現(xiàn)在分享給大家,幫大家做個參考.
本篇我們用一個測試機上的阻塞socket實例來說明主題。文章中所有圖都是在測試系統(tǒng)上現(xiàn)截取的。

需要理解的3個概念

1. TCP socket的buffer

每個TCP socket在內(nèi)核中都有一個發(fā)送緩沖區(qū)和一個接收緩沖區(qū),TCP的全雙工的工作模式以及TCP的流量(擁塞)控制便是依賴于這兩個獨立的buffer以及buffer的填充狀態(tài)。接收緩沖區(qū)把數(shù)據(jù)緩存入內(nèi)核,應(yīng)用進程一直沒有調(diào)用recv()進行讀取的話,此數(shù)據(jù)會一直緩存在相應(yīng)socket的接收緩沖區(qū)內(nèi)。再啰嗦一點,不管進程是否調(diào)用recv()讀取socket,對端發(fā)來的數(shù)據(jù)都會經(jīng)由內(nèi)核接收并且緩存到socket的內(nèi)核接收緩沖區(qū)之中。recv()所做的工作,就是把內(nèi)核緩沖區(qū)中的數(shù)據(jù)拷貝到應(yīng)用層用戶的buffer里面,并返回,僅此而已。進程調(diào)用send()發(fā)送的數(shù)據(jù)的時候,最簡單情況(也是一般情況),將數(shù)據(jù)拷貝進入socket的內(nèi)核發(fā)送緩沖區(qū)之中,然后send便會在上層返回。換句話說,send()返回之時,數(shù)據(jù)不一定會發(fā)送到對端去(和write寫文件有點類似),send()僅僅是把應(yīng)用層buffer的數(shù)據(jù)拷貝進socket的內(nèi)核發(fā)送buffer中,發(fā)送是TCP的事情,和send其實沒有太大關(guān)系。接收緩沖區(qū)被TCP用來緩存網(wǎng)絡(luò)上來的數(shù)據(jù),一直保存到應(yīng)用進程讀走為止。對于TCP,如果應(yīng)用進程一直沒有讀取,接收緩沖區(qū)滿了之后,發(fā)生的動作是:收端通知發(fā)端,接收窗口關(guān)閉(win=0)。這個便是滑動窗口的實現(xiàn)。保證TCP套接口接收緩沖區(qū)不會溢出,從而保證了TCP是可靠傳輸。因為對方不允許發(fā)出超過所通告窗口大小的數(shù)據(jù)。 這就是TCP的流量控制,如果對方無視窗口大小而發(fā)出了超過窗口大小的數(shù)據(jù),則接收方TCP將丟棄它。
查看測試機的socket發(fā)送緩沖區(qū)大小,如圖1所示
圖1 第一個值是一個限制值,socket發(fā)送緩存區(qū)的最少字節(jié)數(shù);
第二個值是默認值;
第三個值是一個限制值,socket發(fā)送緩存區(qū)的最大字節(jié)數(shù);
根據(jù)實際測試,發(fā)送緩沖區(qū)的尺寸在默認情況下的全局設(shè)置是16384字節(jié),即16k。
在測試系統(tǒng)上,發(fā)送緩存默認值是16k。
proc文件系統(tǒng)下的值和sysctl中的值都是全局值,應(yīng)用程序可根據(jù)需要在程序中使用setsockopt()對某個socket的發(fā)送緩沖區(qū)尺寸進行單獨修改,詳見文章《TCP選項之SO_RCVBUF和SO_SNDBUF》,不過這都是題外話。

2. 接收窗口(滑動窗口)

TCP連接建立之時的收端的初始接受窗口大小是14600,細節(jié)如圖2所示(129是收端,130是發(fā)端)
圖2 接收窗口是TCP中的滑動窗口,TCP的收端用這個接受窗口----win=14600,通知發(fā)端,我目前的接收能力是14600字節(jié)。
后續(xù)發(fā)送過程中,收端會不斷的用ACK(ACK的全部作用請參照博文《TCP之ACK發(fā)送情景》)通知發(fā)端自己的接收窗口的大小狀態(tài),如圖3,而發(fā)端發(fā)送數(shù)據(jù)的量,就根據(jù)這個接收窗口的大小來確定,發(fā)端不會發(fā)送超過收端接收能力的數(shù)據(jù)量。這樣就起到了一個流量控制的的作用。
圖3
圖3說明
21,22兩個包都是收端發(fā)給發(fā)端的ACK包
第21個包,收端確認收到的前7240個字節(jié)數(shù)據(jù),7241的意思是期望收到的包從7241號開始,序號加了1.同時,接收窗口從最初的14656(如圖2)經(jīng)過慢啟動階段增加到了現(xiàn)在的29120。用來表明現(xiàn)在收端可以接收29120個字節(jié)的數(shù)據(jù),而發(fā)端看到這個窗口通告,在沒有收到新的ACK的時候,發(fā)端可以向收端發(fā)送29120字節(jié)這么多數(shù)據(jù)。
第22個包,收端確認收到的前8688個字節(jié)數(shù)據(jù),并通告自己的接收窗口繼續(xù)增長為32000這么大。

3. 單個TCP的負載量和MSS的關(guān)系

MSS在以太網(wǎng)上通常大小是1460字節(jié),而我們在后續(xù)發(fā)送過程中的單個TCP包的最大數(shù)據(jù)承載量是1448字節(jié),這二者的關(guān)系可以參考博文《TCP之1460MSS和1448負載》。

實例詳解send()

實例功能說明:接收端129作為客戶端去連接發(fā)送端130,連接上之后并不調(diào)用recv()接收,而是sleep(1000),把進程暫停下來,不讓進程接收數(shù)據(jù)。內(nèi)核會緩存數(shù)據(jù)至接收緩沖區(qū)。發(fā)送端作為服務(wù)器接收TCP請求之后,立即用ret = send(sock,buf,70k,0);這個C語句,向接收端發(fā)送70k數(shù)據(jù)。
我們現(xiàn)在來觀察這個過程。看看究竟發(fā)生了些什么事。wireshark抓包截圖如下圖4
圖4
圖4說明,包序號等同于時序
1. 客戶端sleep在recv()之前,目的是為了把數(shù)據(jù)壓入接收緩沖區(qū)。服務(wù)端調(diào)用"ret = send(sock,buf,70k,0);"這個C語句,向接收端發(fā)送70k數(shù)據(jù)。由于發(fā)送緩沖區(qū)大小16k,send()無法將70k數(shù)據(jù)全部拷貝進發(fā)送緩沖區(qū),故先拷貝16k進入發(fā)送緩沖區(qū),下層發(fā)送緩沖區(qū)中有數(shù)據(jù)要發(fā)送,內(nèi)核開始發(fā)送。上層send()在應(yīng)用層處于阻塞狀態(tài);
2. 11號TCP包,發(fā)端從這兒開始向收端發(fā)送1448個字節(jié)的數(shù)據(jù);
3. 12號TCP包,發(fā)端沒有收到之前發(fā)送的1448個數(shù)據(jù)的ACK包,仍然繼續(xù)向收端發(fā)送1448個字節(jié)的數(shù)據(jù);
4. 13號TCP包,收端向發(fā)端發(fā)送1448字節(jié)的確認包,表明收端成功接收總共1448個字節(jié)。此時收端并未調(diào)用recv()讀取,目前發(fā)送緩沖區(qū)中被壓入1448字節(jié)。由于處于慢啟動狀態(tài),win接收窗口持續(xù)增大,表明接受能力在增加,吞吐量持續(xù)上升;
5. 14號TCP包,收端向發(fā)端發(fā)送2896字節(jié)的確認包,表明收端成功接收總共2896個字節(jié)。此時收端并未調(diào)用recv()讀取,目前發(fā)送緩沖區(qū)中被壓入2896字節(jié)。由于處于慢啟動狀態(tài),win接收窗口持續(xù)增大,表明接受能力在增加,吞吐量持續(xù)上升;
6. 15號TCP包,發(fā)端繼續(xù)向收端發(fā)送1448個字節(jié)的數(shù)據(jù);
7. 16號TCP包,收端向發(fā)端發(fā)送4344字節(jié)的確認包,表明收端成功接收總共4344個字節(jié)。此時收端并未調(diào)用recv()讀取,目前發(fā)送緩沖區(qū)中被壓入4344字節(jié)。由于處于慢啟動狀態(tài),win接收窗口持續(xù)增大,表明接受能力在增加,吞吐量持續(xù)上升;
8. 從這兒開始,我略去很多包,過程類似上面過程。同時,由于不斷的發(fā)送出去的數(shù)據(jù)被收端用ACK確認,發(fā)送緩沖區(qū)的空間被逐漸騰出空地,send()內(nèi)部不斷的把應(yīng)用層buf中的數(shù)據(jù)向發(fā)送緩沖區(qū)拷貝,從而不斷的發(fā)送,過程重復(fù)。70k數(shù)據(jù)并沒有被完全送入內(nèi)核,send()不管是否發(fā)送出去,send不管發(fā)送出去的是否被確認,send()只關(guān)心buf中的數(shù)據(jù)有沒有被全部送往發(fā)送緩沖區(qū)。如果buf中的數(shù)據(jù)沒有被全部送往發(fā)送緩沖區(qū),send()在應(yīng)用層阻塞,負責(zé)等待發(fā)送緩沖區(qū)中有空余空間的時候,逐步拷貝buf中的數(shù)據(jù);如果buf中的數(shù)據(jù)被全部拷入發(fā)送緩沖區(qū),send()立即返回。
9. 經(jīng)過慢啟動階段接收窗口增大到穩(wěn)定階段,TCP吞吐量升高到穩(wěn)定階段,收端一直處于sleep狀態(tài),沒有調(diào)用recv()把內(nèi)核中接收緩沖區(qū)中的數(shù)據(jù)拷貝到應(yīng)用層去,此時收端的接收緩沖區(qū)中被壓入大量數(shù)據(jù);
10. 66號、67號TCP數(shù)據(jù)包,發(fā)端繼續(xù)向收端發(fā)送數(shù)據(jù);
11. 68號TCP數(shù)據(jù)包,收端發(fā)送ACK包確認接收到的數(shù)據(jù),ACK=62265表明收端已經(jīng)收到62265字節(jié)的數(shù)據(jù),這些數(shù)據(jù)目前被壓在收端的接收緩沖區(qū)中。win=3456,比較之前的16號TCP包的win=23296,表明收端的窗口已經(jīng)處于收縮狀態(tài),收端的接收緩沖區(qū)中的數(shù)據(jù)遲遲未被應(yīng)用層讀走,導(dǎo)致接收緩沖區(qū)空間吃緊,故收縮窗口,控制發(fā)送端的發(fā)送量,進行流量控制;
12. 69號、70號TCP數(shù)據(jù)包,發(fā)端在接收窗口允許的數(shù)據(jù)量的范圍內(nèi),繼續(xù)向收端發(fā)送2段1448字節(jié)長度的數(shù)據(jù);
13. 71號TCP數(shù)據(jù)包,至此,收端已經(jīng)成功接收65160字節(jié)的數(shù)據(jù),全部被壓在接收緩沖區(qū)之中,接收窗口繼續(xù)收縮,尺寸為1600字節(jié);
14. 72號TCP數(shù)據(jù)包,發(fā)端在接收窗口允許的數(shù)據(jù)量的范圍內(nèi),繼續(xù)向收端發(fā)送1448字節(jié)長度的數(shù)據(jù);
15. 73號TCP數(shù)據(jù)包,至此,收端已經(jīng)成功接收66609字節(jié)的數(shù)據(jù),全部被壓在接收緩沖區(qū)之中,接收窗口繼續(xù)收縮,尺寸為192字節(jié)。
16. 74號TCP數(shù)據(jù)包,和我們這個例子沒有關(guān)系,是別的應(yīng)用發(fā)送的包;
17. 75號TCP數(shù)據(jù)包,發(fā)端在接收窗口允許的數(shù)據(jù)量的范圍內(nèi),向收端發(fā)送192字節(jié)長度的數(shù)據(jù);
18. 76號TCP數(shù)據(jù)包,至此,收端已經(jīng)成功接收66609字節(jié)的數(shù)據(jù),全部被壓在接收緩沖區(qū)之中,win=0接收窗口關(guān)閉,接收緩沖區(qū)滿,無法再接收任何數(shù)據(jù);
19. 77號、78號、79號TCP數(shù)據(jù)包,由keepalive觸發(fā)的數(shù)據(jù)包,響應(yīng)的ACK持有接收窗口的狀態(tài)win=0,另外,ACK=66801表明接收端的接收緩沖區(qū)中積壓了66800字節(jié)的數(shù)據(jù)。
20. 從以上過程,我們應(yīng)該熟悉了滑動窗口通告字段win所說明的問題,以及ACK確認數(shù)據(jù)等等。現(xiàn)在可得出一個結(jié)論,接收端的接收緩存尺寸應(yīng)該是66800字節(jié)(此結(jié)論并非本篇主題)。
send()要發(fā)送的數(shù)據(jù)是70k,現(xiàn)在發(fā)出去了66800字節(jié),發(fā)送緩存中還有16k,應(yīng)用層剩余要拷貝進內(nèi)核的數(shù)據(jù)量是N=70k-66800-16k。接收端仍處于sleep狀態(tài),無法recv()數(shù)據(jù),這將導(dǎo)致接收緩沖區(qū)一直處于積壓滿的狀態(tài),窗口會一直通告0(win=0)。發(fā)送端在這樣的狀態(tài)下徹底無法發(fā)送數(shù)據(jù)了,send()的剩余數(shù)據(jù)無法繼續(xù)拷貝進內(nèi)核的發(fā)送緩沖區(qū),最終導(dǎo)致send()被阻塞在應(yīng)用層;
21. send()一直阻塞中。。。

圖4和send()的關(guān)系說明完畢。
那什么時候send返回呢?有3種返回場景

send()返回場景

場景1,我們繼續(xù)圖4這個例子,不過這兒開始我們就跳出圖4所示的過程了

22. 接收端sleep(1000)到時間了,進程被喚醒,代碼片段如圖5

圖5
隨著進程不斷的用"recv(fd,buf,2048,0);"將數(shù)據(jù)從內(nèi)核的接收緩沖區(qū)拷貝至應(yīng)用層的buf,在使用win=0關(guān)閉接收窗口之后,現(xiàn)在接收緩沖區(qū)又逐漸恢復(fù)了緩存的能力,這個條件下,收端會主動發(fā)送攜帶"win=n(n>0)"這樣的ACK包去通告發(fā)送端接收窗口已打開; 23. 發(fā)端收到攜帶"win=n(n>0)"這樣的ACK包之后,開始繼續(xù)在窗口運行的數(shù)據(jù)量范圍內(nèi)發(fā)送數(shù)據(jù)。發(fā)送緩沖區(qū)的數(shù)據(jù)被發(fā)出;
24. 收端繼續(xù)接收數(shù)據(jù),并用ACK確認這些數(shù)據(jù);
25. 發(fā)端收到ACK,可以清理出一些發(fā)送緩沖區(qū)空間,應(yīng)用層send()的剩余數(shù)據(jù)又可以被不斷的拷貝進內(nèi)核的發(fā)送緩沖區(qū);
26. 不斷重復(fù)以上發(fā)送過程;
27. send()的70k數(shù)據(jù)全部進入內(nèi)核,send()成功返回。

場景2,我們繼續(xù)圖4這個例子,不過這兒開始我們就跳出圖4所示的過程了
22. 收端進程或者socket出現(xiàn)問題,給發(fā)端發(fā)送一個RST,請參考博文《》;
23. 內(nèi)核收到RST,send返回-1。

場景3,和以上例子沒關(guān)系
連接上之后,馬上send(1k),這樣,發(fā)送的數(shù)據(jù)肯定可以一次拷貝進入發(fā)送緩沖區(qū),send()拷貝完數(shù)據(jù)立即成功返回。

send()發(fā)送結(jié)論

其實場景1和場景2說明一個問題
send()只是負責(zé)拷貝,拷貝完立即返回,不會等待發(fā)送和發(fā)送之后的ACK。如果socket出現(xiàn)問題,RST包被反饋回來。在RST包返回之時,如果send()還沒有把數(shù)據(jù)全部放入內(nèi)核或者發(fā)送出去,那么send()返回-1,errno被置錯誤值;如果RST包返回之時,send()已經(jīng)返回,那么RST導(dǎo)致的錯誤會在下一次send()或者recv()調(diào)用的時候被立即返回。
場景3完全說明send()只要完成拷貝就成功返回,如果發(fā)送數(shù)據(jù)的過程中出現(xiàn)各種錯誤,下一次send()或者recv()調(diào)用的時候被立即返回。

概念上容易疑惑的地方

1. TCP協(xié)議本身是為了保證可靠傳輸,并不等于應(yīng)用程序用tcp發(fā)送數(shù)據(jù)就一定是可靠的,必須要容錯;
2. send()和recv()沒有固定的對應(yīng)關(guān)系,不定數(shù)目的send()可以觸發(fā)不定數(shù)目的recv(),這話不專業(yè),但是還是必須說一下,初學(xué)者容易疑惑;
3. 關(guān)鍵點,send()只負責(zé)拷貝,拷貝到內(nèi)核就返回,我通篇在說拷貝完返回,很多文章中說send()在成功發(fā)送數(shù)據(jù)后返回,成功發(fā)送是說發(fā)出去的東西被ACK確認過。send()只拷貝,不會等ACK;
4. 此次send()調(diào)用所觸發(fā)的程序錯誤,可能會在本次返回,也可能在下次調(diào)用網(wǎng)絡(luò)IO函數(shù)的時候被返回。


實際上理解了阻塞式的,就能理解非阻塞的。 《新程序員》:云原生和全面數(shù)字化實踐50位技術(shù)專家共同創(chuàng)作,文字、視頻、音頻交互閱讀

總結(jié)

以上是生活随笔為你收集整理的TCP之深入浅出send和recv的全部內(nèi)容,希望文章能夠幫你解決所遇到的問題。

如果覺得生活随笔網(wǎng)站內(nèi)容還不錯,歡迎將生活随笔推薦給好友。