TCP之深刻淺出send和recv

在這篇文章中,我用深刻淺出socket選項行爲中的4個選項來介紹send和recv的行爲

SO_RCVBUF SO_SNDBUF

先明確一個概念:每一個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都有一個接收緩衝區,沒有發送緩衝區,從概念上來講就是隻要有數據就發,無論對方是否能夠正確接收,因此不緩衝,不須要發送緩衝區。 node

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

TCP_CORK TCP_NODELAY

這兩個選項是互斥的,打開或者關閉TCP的nagle算法,下面用場景來解釋 算法

典型的webserver向客戶端的應答,應用層代碼實現流程粗略來講,通常以下所示: 緩存

  if(條件1{ 服務器

     buffer_last_modified填充協議內容「Last-Modified: Sat, 04 May 2012 05:28:58 GMT」; 網絡

     sendbuffer_last_modified); socket

  } 性能

 

   if(條件2{ 測試

     buffer_expires填充協議內容「Expires: Mon, 14 Aug 2023 05:17:29 GMT」; spa

     sendbuffer_expires);

   }

 

   。。。

   if(條件N{

     buffer_N填充協議內容「。。。」;

     sendbuffer_N);

   }

 

對於這樣的實現,當前的http應答在執行這段代碼時,假設有M(M<=N)個條件都知足,那麼會有連續的M個send調用,那是否是下層會依次向客戶端發出M個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拼音二聲)包。

相關文章
相關標籤/搜索