TCP之深入淺出send和recv

轉自:http://blog.chinaunix.net/uid-29075379-id-3921527.html

需要理解的3個概念

1. TCP socket的buffer

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

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

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

3. 單個TCP的負載量和MSS的關係

MSS在以太網上通常大小是1460字節,而我們在後續發送過程中的單個TCP包的最大數據承載量是1448字節,這二者的關係可以參考博文《TCP之1460MSS和1448負載》。

實例詳解send()

實例功能說明:接收端129作爲客戶端去連接發送端130,連接上之後並不調用recv()接收,而是sleep(1000),把進程暫停下來,不讓進程接收數據。內核會緩存數據至接收緩衝區。發送端作爲服務器接收TCP請求之後,立即用ret = send(sock,buf,70k,0);這個C語句,向接收端發送70k數據。
我們現在來觀察這個過程。看看究竟發生了些什麼事。wireshark抓包截圖如下圖4
圖4

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

圖4和send()的關係說明完畢。
那什麼時候send返回呢?有3種返回場景

send()返回場景

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

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

圖5
隨着進程不斷的用"recv(fd,buf,2048,0);"將數據從內核的接收緩衝區拷貝至應用層的buf,在使用win=0關閉接收窗口之後,現在接收緩衝區又逐漸恢復了緩存的能力,這個條件下,收端會主動發送攜帶"win=n(n>0)"這樣的ACK包去通告發送端接收窗口已打開;
23. 發端收到攜帶"win=n(n>0)"這樣的ACK包之後,開始繼續在窗口運行的數據量範圍內發送數據。發送緩衝區的數據被髮出;
24. 收端繼續接收數據,並用ACK確認這些數據;
25. 發端收到ACK,可以清理出一些發送緩衝區空間,應用層send()的剩餘數據又可以被不斷的拷貝進內核的發送緩衝區;
26. 不斷重複以上發送過程;
27. send()的70k數據全部進入內核,send()成功返回。

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

場景3,和以上例子沒關係
連接上之後,馬上send(1k),這樣,發送的數據肯定可以一次拷貝進入發送緩衝區,send()拷貝完數據立即成功返回。

send()發送結論

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

概念上容易疑惑的地方

1. TCP協議本身是爲了保證可靠傳輸,並不等於應用程序用tcp發送數據就一定是可靠的,必須要容錯;
2. send()和recv()沒有固定的對應關係,不定數目的send()可以觸發不定數目的recv(),這話不專業,但是還是必須說一下,初學者容易疑惑;
3. 關鍵點,send()只負責拷貝,拷貝到內核就返回,我通篇在說拷貝完返回,很多文章中說send()在成功發送數據後返回,成功發送是說發出去的東西被ACK確認過。send()只拷貝,不會等ACK;
4. 此次send()調用所觸發的程序錯誤,可能會在本次返回,也可能在下次調用網絡IO函數的時候被返回。


實際上理解了阻塞式的,就能理解非阻塞的。


發佈了20 篇原創文章 · 獲贊 20 · 訪問量 3萬+
發表評論
所有評論
還沒有人評論,想成為第一個評論的人麼? 請在上方評論欄輸入並且點擊發布.
相關文章