深入淺出TCP之send和recv
先明確一個概念:每個TCP socket在內(nèi)核中都有一個發(fā)送緩沖區(qū)和一個接收緩沖區(qū),TCP的全雙工的工作模式以及TCP的滑動窗口便是依賴于這兩個獨立的buffer以及此buffer的填充狀態(tài)。接收緩沖區(qū)把數(shù)據(jù)緩存入內(nèi)核,應用進程一直沒有調(diào)用read進行讀取的話,此數(shù)據(jù)會一直緩存在相應 socket的接收緩沖區(qū)內(nèi)。再啰嗦一點,不管進程是否讀取socket,對端發(fā)來的數(shù)據(jù)都會經(jīng)由內(nèi)核接收并且緩存到socket的內(nèi)核接收緩沖區(qū)之中。 read所做的工作,就是把內(nèi)核緩沖區(qū)中的數(shù)據(jù)拷貝到應用層用戶的buffer里面,僅此而已。進程調(diào)用send發(fā)送的數(shù)據(jù)的時候,最簡單情況(也是一般情況),將數(shù)據(jù)拷貝進入socket的內(nèi)核發(fā)送緩沖區(qū)之中,然后send便會在上層返回。換句話說,send返回之時,數(shù)據(jù)不一定會發(fā)送到對端去(和 write寫文件有點類似),send僅僅是把應用層buffer的數(shù)據(jù)拷貝進socket的內(nèi)核發(fā)送buffer中。后續(xù)我會專門用一篇文章介紹 read和send所關(guān)聯(lián)的內(nèi)核動作。每個UDP socket都有一個接收緩沖區(qū),沒有發(fā)送緩沖區(qū),從概念上來說就是只要有數(shù)據(jù)就發(fā),不管對方是否可以正確接收,所以不緩沖,不需要發(fā)送緩沖區(qū)。
接收緩沖區(qū)被TCP和UDP用來緩存網(wǎng)絡(luò)上來的數(shù)據(jù),一直保存到應用進程讀走為止。對于TCP,如果應用進程一直沒有讀取,buffer滿了之后,發(fā)生的動作是:通知對端TCP協(xié)議中的窗口關(guān)閉。這個便是滑動窗口的實現(xiàn)。保證TCP套接口接收緩沖區(qū)不會溢出,從而保證了TCP是可靠傳輸。因為對方不允許發(fā)出超過所通告窗口大小的數(shù)據(jù)。 這就是TCP的流量控制,如果對方無視窗口大小而發(fā)出了超過窗口大小的數(shù)據(jù),則接收方TCP將丟棄它。 UDP:當套接口接收緩沖區(qū)滿時,新來的數(shù)據(jù)報無法進入接收緩沖區(qū),此數(shù)據(jù)報就被丟棄。UDP是沒有流量控制的;快的發(fā)送者可以很容易地就淹沒慢的接收者,導致接收方的UDP丟棄數(shù)據(jù)報。
以上便是TCP可靠,UDP不可靠的實現(xiàn)。
TCP_CORK TCP_NODELAY
這兩個選項是互斥的,打開或者關(guān)閉TCP的nagle算法,下面用場景來解釋
典型的webserver向客戶端的應答,應用層代碼實現(xiàn)流程粗略來說,一般如下所示:
if(條件1){
向buffer_last_modified填充協(xié)議內(nèi)容“Last-Modified: Sat, 04 May 2012 05:28:58 GMT”;
send(buffer_last_modified);
}
if(條件2){
向buffer_expires填充協(xié)議內(nèi)容“Expires: Mon, 14 Aug 2023 05:17:29 GMT”;
send(buffer_expires);
}
。。。
if(條件N){
向buffer_N填充協(xié)議內(nèi)容“。。。”;
send(buffer_N);
}
對于這樣的實現(xiàn),當前的http應答在執(zhí)行這段代碼時,假設(shè)有M(M<=N)個條件都滿足,那么會有連續(xù)的M個send調(diào)用,那是不是下層會依次向客戶端發(fā)出M個TCP包呢?答案是否定的,包的數(shù)目在應用層是無法控制的,并且應用層也是不需要控制的。
我用下列四個假設(shè)場景來解釋一下這個答案
由于TCP是流式的,對于TCP而言,每個TCP連接只有syn開始和fin結(jié)尾,中間發(fā)送的數(shù)據(jù)是沒有邊界的,多個連續(xù)的send所干的事情僅僅是:
假如socket的文件描述符被設(shè)置為阻塞方式,而且發(fā)送緩沖區(qū)還有足夠空間容納這個send所指示的應用層buffer的全部數(shù)據(jù),那么把這些數(shù)據(jù)從應用層的buffer,拷貝到內(nèi)核的發(fā)送緩沖區(qū),然后返回。
假如socket的文件描述符被設(shè)置為阻塞方式,但是發(fā)送緩沖區(qū)沒有足夠空間容納這個send所指示的應用層buffer的全部數(shù)據(jù),那么能拷貝多少就拷貝多少,然后進程掛起,等到TCP對端的接收緩沖區(qū)有空余空間時,通過滑動窗口協(xié)議(ACK包的又一個作用----打開窗口)通知TCP本端:“親,我已經(jīng)做好準備,您現(xiàn)在可以繼續(xù)向我發(fā)送X個字節(jié)的數(shù)據(jù)了”,然后本端的內(nèi)核喚醒進程,繼續(xù)向發(fā)送緩沖區(qū)拷貝剩余數(shù)據(jù),并且內(nèi)核向TCP對端發(fā)送TCP數(shù)據(jù),如果send所指示的應用層buffer中的數(shù)據(jù)在本次仍然無法全部拷貝完,那么過程重復。。。直到所有數(shù)據(jù)全部拷貝完,返回。
請注意,對于send的行為,我用了“拷貝一次”,send和下層是否發(fā)送數(shù)據(jù)包,沒有任何關(guān)系。
假如socket的文件描述符被設(shè)置為非阻塞方式,而且發(fā)送緩沖區(qū)還有足夠空間容納這個send所指示的應用層buffer的全部數(shù)據(jù),那么把這些數(shù)據(jù)從應用層的buffer,拷貝到內(nèi)核的發(fā)送緩沖區(qū),然后返回。
假如socket的文件描述符被設(shè)置為非阻塞方式,但是發(fā)送緩沖區(qū)沒有足夠空間容納這個send所指示的應用層buffer的全部數(shù)據(jù),那么能拷貝多少就拷貝多少,然后返回拷貝的字節(jié)數(shù)。多涉及一點,返回之后有兩種處理方式:
1.死循環(huán),一直調(diào)用send,持續(xù)測試,一直到結(jié)束(基本上不會這么搞)。
2.非阻塞搭配epoll或者select,用這兩種東西來測試socket是否達到可發(fā)送的活躍狀態(tài),然后調(diào)用send(高性能服務器必需的處理方式)。
綜上,以及請參考本文前述的SO_RCVBUF和SO_SNDBUF,你會發(fā)現(xiàn),在實際場景中,你能發(fā)出多少TCP包以及每個包承載多少數(shù)據(jù),除了受到自身服務器配置和環(huán)境帶寬影響,對端的接收狀態(tài)也能影響你的發(fā)送狀況。
至于為什么說“應用層也是不需要控制發(fā)送行為的”,這個說法的原因是:
軟件系統(tǒng)分層處理、分模塊處理各種軟件行為,目的就是為了各司其職,分工。應用層只關(guān)心業(yè)務實現(xiàn),控制業(yè)務。數(shù)據(jù)傳輸由專門的層面去處理,這樣應用層開發(fā)的規(guī)模和復雜程度會大為降低,開發(fā)和維護成本也會相應降低。
再回到發(fā)送的話題上來:)之前說應用層無法精確控制和完全控制發(fā)送行為,那是不是就是不控制了?非也!雖然無法控制,但也要盡量控制!
如何盡量控制?現(xiàn)在引入本節(jié)主題----TCP_CORK和TCP_NODELAY。
cork:塞子,塞住
nodelay:不要延遲
TCP_CORK:盡量向發(fā)送緩沖區(qū)中攢數(shù)據(jù),攢到多了再發(fā)送,這樣網(wǎng)絡(luò)的有效負載會升高。簡單粗暴地解釋一下這個有效負載的問題。假如每個包中只有一個字節(jié)的數(shù)據(jù),為了發(fā)送這一個字節(jié)的數(shù)據(jù),再給這一個字節(jié)外面包裝一層厚厚的TCP包頭,那網(wǎng)絡(luò)上跑的幾乎全是包頭了,有效的數(shù)據(jù)只占其中很小的部分,很多訪問量大的服務器,帶寬可以很輕松的被這么耗盡。那么,為了讓有效負載升高,我們可以通過這個選項指示TCP層,在發(fā)送的時候盡量多攢一些數(shù)據(jù),把他們填充到一個TCP包中再發(fā)送出去。這個和提升發(fā)送效率是相互矛盾的,空間和時間總是一堆冤家!!
TCP_NODELAY:盡量不要等待,只要發(fā)送緩沖區(qū)中有數(shù)據(jù),并且發(fā)送窗口是打開的,就盡量把數(shù)據(jù)發(fā)送到網(wǎng)絡(luò)上去。
很明顯,兩個選項是互斥的。實際場景中該怎么選擇這兩個選項呢?再次舉例說明
webserver,,下載服務器(ftp的發(fā)送文件服務器),需要帶寬量比較大的服務器,用TCP_CORK。
涉及到交互的服務器,比如ftp的接收命令的服務器,必須使用TCP_NODELAY。默認是TCP_CORK。設(shè)想一下,用戶每次敲幾個字節(jié)的命令,而下層在攢這些數(shù)據(jù),想等到數(shù)據(jù)量多了再發(fā)送,這樣用戶會等到發(fā)瘋。這個糟糕的場景有個專門的詞匯來形容-----粘(nian拼音二聲)包。
原文博客:http://blog.chinaunix.net/uid-29075379-id-3895700.html