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