Linux-TCP之深刻淺出send和recv【轉】

轉自:http://www.javashuo.com/article/p-vcoaxyih-ce.htmlhtml

內容摘自:TCP之深刻淺出send和recv再次深刻理解TCP網絡編程中的send和recvnode

建議閱讀時參考:Unix環境高級編程-TCP、UDP緩衝區web

概念編程

  先明確一個概念:每一個TCP socket在內核中都有一個發送緩衝區和一個接收緩衝區,TCP的全雙工的工做模式以及TCP的滑動窗口即是依賴於這兩個獨立的buffer以及此buffer的填充狀態。接收緩衝區把數據緩存入內核,應用進程一直沒有調用read進行讀取的話,此數據會一直緩存在相應 socket的接收緩衝區內。再囉嗦一點,無論進程是否讀取socket,對端發來的數據都會經由內核接收而且緩存到socket的內核接收緩衝區之中。 read所作的工做,就是把內核緩衝區中的數據拷貝到應用層用戶的buffer裏面,僅此而已。進程調用send發送的數據的時候,最簡單狀況(也是通常狀況),將數據拷貝進入socket的內核發送緩衝區之中,而後send便會在上層返回。換句話說,send返回之時,數據不必定會發送到對端去(和 write寫文件有點相似),send僅僅是把應用層buffer的數據拷貝進socket的內核發送buffer中。後續我會專門用一篇文章介紹 read和send所關聯的內核動做。每一個UDP socket都有一個接收緩衝區,沒有發送緩衝區,從概念上來講就是隻要有數據就發,無論對方是否能夠正確接收,因此不緩衝,不須要發送緩衝區。緩存

  接收緩衝區被TCP和UDP用來緩存網絡上來的數據,一直保存到應用進程讀走爲止。對於TCP,若是應用進程一直沒有讀取,buffer滿了以後,發生的動做是:通知對端TCP協議中的窗口關閉。這個即是滑動窗口的實現。保證TCP套接口接收緩衝區不會溢出,從而保證了TCP是可靠傳輸。由於對方不容許發出超過所通告窗口大小的數據。 這就是TCP的流量控制,若是對方無視窗口大小而發出了超過窗口大小的數據,則接收方TCP將丟棄它。 UDP:當套接口接收緩衝區滿時,新來的數據報沒法進入接收緩衝區,此數據報就被丟棄。UDP是沒有流量控制的;快的發送者能夠很容易地就淹沒慢的接收者,致使接收方的UDP丟棄數據報。服務器

  以上即是TCP可靠,UDP不可靠的實現。網絡

複製代碼
if(條件1){
    向buffer_last_modified填充協議內容「Last-Modified: Sat, 04 May 2012 05:28:58 GMT」;
    send(buffer_last_modified);
}
if(條件2){
    向buffer_expires填充協議內容「Expires: Mon, 14 Aug 2023 05:17:29 GMT」;
    send(buffer_expires);
}
if(條件N){
    向buffer_N填充協議內容「。。。」;
    send(buffer_N);
}
複製代碼

  對於這樣的實現,當前的http應答在執行這段代碼時,假設有M(M<=N)個條件都知足,那麼會有連續的M個send調用,那是否是下層會依次向客戶端發出M個TCP包呢?答案是否認的,包的數目在應用層是沒法控制的,而且應用層也是不須要控制的socket

  用下列四個假設場景來解釋一下這個答案:tcp

    因爲TCP是流式的,對於TCP而言,每一個TCP鏈接只有syn開始和fin結尾,中間發送的數據是沒有邊界的,多個連續的send所幹的事情僅僅是:函數

      假如socket的文件描述符被設置爲阻塞方式,並且發送緩衝區還有足夠空間容納這個send所指示的應用層buffer的所有數據,那麼把這些數據從應用層的buffer,拷貝到內核的發送緩衝區,而後返回。

      假如socket的文件描述符被設置爲阻塞方式,可是發送緩衝區沒有足夠空間容納這個send所指示的應用層buffer的所有數據,那麼能拷貝多少就拷貝多少,而後進程掛起,等到TCP對端的接收緩衝區有空餘空間時,經過滑動窗口協議(ACK包的又一個做用----打開窗口)通知TCP本端:「親,我已經作好準備,您如今能夠繼續向我發送X個字節的數據了」,而後本端的內核喚醒進程,繼續向發送緩衝區拷貝剩餘數據,而且內核向TCP對端發送TCP數據,若是send所指示的應用層buffer中的數據在本次仍然沒法所有拷貝完,那麼過程重複。。。直到全部數據所有拷貝完,返回。請注意,對於send的行爲,我用了「拷貝一次」,send和下層是否發送數據包,沒有任何關係。

      假如socket的文件描述符被設置爲非阻塞方式,並且發送緩衝區還有足夠空間容納這個send所指示的應用層buffer的所有數據,那麼把這些數據從應用層的buffer,拷貝到內核的發送緩衝區,而後返回。

      假如socket的文件描述符被設置爲非阻塞方式,可是發送緩衝區沒有足夠空間容納這個send所指示的應用層buffer的所有數據,那麼能拷貝多少就拷貝多少,而後返回拷貝的字節數。多涉及一點,返回以後有兩種處理方式:

        1.死循環,一直調用send,持續測試,一直到結束(基本上不會這麼搞)。

        2.非阻塞搭配epoll或者select,用這兩種東西來測試socket是否達到可發送的活躍狀態,而後調用send(高性能服務器必需的處理方式)。

    綜上,以及請參考本文前述的SO_RCVBUF和SO_SNDBUF,你會發現,在實際場景中,你能發出多少TCP包以及每一個包承載多少數據,除了受到自身服務器配置和環境帶寬影響,對端的接收狀態也能影響你的發送情況。

 

  至於爲何說「應用層也是不須要控制發送行爲的」,這個說法的緣由是:

    軟件系統分層處理、分模塊處理各類軟件行爲,目的就是爲了各司其職,分工。應用層只關心業務實現,控制業務。數據傳輸由專門的層面去處理,這樣應用層開發的規模和複雜程度會大爲下降,開發和維護成本也會相應下降。

  

  再回到發送的話題上來:)以前說應用層沒法精確控制和徹底控制發送行爲,那是否是就是不控制了?非也!雖然沒法控制,但也要儘可能控制!

  如何儘可能控制?如今引入本節主題----TCP_CORK和TCP_NODELAY。

    cork:塞子,塞住

    nodelay:不要延遲

    TCP_CORK:儘可能向發送緩衝區中攢數據,攢到多了再發送,這樣網絡的有效負載會升高。簡單粗暴地解釋一下這個有效負載的問題。假如每一個包中只有一個字節的數據,爲了發送這一個字節的數據,再給這一個字節外面包裝一層厚厚的TCP包頭,那網絡上跑的幾乎全是包頭了,有效的數據只佔其中很小的部分,不少訪問量大的服務器,帶寬能夠很輕鬆的被這麼耗盡。那麼,爲了讓有效負載升高,咱們能夠經過這個選項指示TCP層,在發送的時候儘可能多攢一些數據,把他們填充到一個TCP包中再發送出去。這個和提高發送效率是相互矛盾的,空間和時間老是一堆冤家!!

    TCP_NODELAY:儘可能不要等待,只要發送緩衝區中有數據,而且發送窗口是打開的,就儘可能把數據發送到網絡上去。

  很明顯,兩個選項是互斥的。實際場景中該怎麼選擇這兩個選項呢?再次舉例說明

    webserver,,下載服務器(ftp的發送文件服務器),須要帶寬量比較大的服務器,用TCP_CORK。

    涉及到交互的服務器,好比ftp的接收命令的服務器,必須使用TCP_NODELAY。默認是TCP_CORK。設想一下,用戶每次敲幾個字節的命令,而下層在攢這些數據,想等到數據量多了再發送,這樣用戶會等到發瘋。這個糟糕的場景有個專門的詞彙來形容-----粘(nian拼音二聲)包

 

接下來咱們用一個測試機上的阻塞socket實例來講明主題。文章中全部圖都是在測試系統上現截取的。

須要理解的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函數的時候被返回

 

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

相關文章
相關標籤/搜索