socket使用TCP協議時,send、recv函數解析以及TCP連接關閉的問題


Tcp協議本身是可靠的,並不等於應用程序用tcp發送數據就一定是可靠的.不管是否阻塞,send發送的大小,並不代表對端recv到多少的數據.

在阻塞模式下, send函數的過程是將應用程序請求發送的數據拷貝到發送緩存中發送並得到確認后再返回.但由於發送緩存的存在,表現為:如果發送緩存大小比請求發送的大小要大,那么send函數立即返回,同時向網絡中發送數據;否則,send向網絡發送緩存中不能容納的那部分數據,並等待對端確認后再返回(接收端只要將數據收到接收緩存中,就會確認,並不一定要等待應用程序調用recv);

在非阻塞模式下,send函數的過程僅僅是將數據拷貝到協議棧的緩存區而已,如果緩存區可用空間不夠,則盡能力的拷貝,返回成功拷貝的大小;如緩存區可用空間為0,則返回-1,同時設置errno為EAGAIN.


linux下可用sysctl -a | grep net.ipv4.tcp_wmem查看系統默認的發送緩存大小:
net.ipv4.tcp_wmem = 4096 16384 81920
這有三個值,第一個值是socket的發送緩存區分配的最少字節數,第二個值是默認值(該值會被net.core.wmem_default覆蓋),緩存區在系統負載不重的情況下可以增長到這個值,第三個值是發送緩存區空間的最大字節數(該值會被net.core.wmem_max覆蓋).
根據實際測試,如果手工更改了net.ipv4.tcp_wmem的值,則會按更改的值來運行,否則在默認情況下,協議棧通常是按net.core.wmem_default和net.core.wmem_max的值來分配內存的.

應用程序應該根據應用的特性在程序中更改發送緩存大小:

socklen_t sendbuflen = 0;
socklen_t len = sizeof(sendbuflen);
getsockopt(clientSocket, SOL_SOCKET, SO_SNDBUF, (void*)&sendbuflen, &len);
printf("default,sendbuf:%d/n", sendbuflen);

sendbuflen = 10240;
setsockopt(clientSocket, SOL_SOCKET, SO_SNDBUF, (void*)&sendbuflen, len);
getsockopt(clientSocket, SOL_SOCKET, SO_SNDBUF, (void*)&sendbuflen, &len);
printf("now,sendbuf:%d/n", sendbuflen);


需要注意的是,雖然將發送緩存設置成了10k,但實際上,協議棧會將其擴大1倍,設為20k.
-------------------實例分析---------------

在實際應用中,如果發送端是非阻塞發送,由於網絡的阻塞或者接收端處理過慢,通常出現的情況是,發送應用程序看起來發送了10k的數據,但是只發送了2k到對端緩存中,還有8k在本機緩存中(未發送或者未得到接收端的確認).那么此時,接收應用程序能夠收到的數據為2k.假如接收應用程序調用recv函數獲取了1k的數據在處理,在這個瞬間,發生了以下情況之一,雙方表現為:

A. 發送應用程序認為send完了10k數據,關閉了socket:
發送主機作為tcp的主動關閉者,連接將處於FIN_WAIT1的半關閉狀態(等待對方的ack),並且,發送緩存中的8k數據並不清除,依然會發送給對端.如果接收應用程序依然在recv,那么它會收到余下的8k數據(這個前題是,接收端會在發送端FIN_WAIT1狀態超時前收到余下的8k數據.), 然后得到一個對端socket被關閉的消息(recv返回0).這時,應該進行關閉.

B. 發送應用程序再次調用send發送8k的數據:
假如發送緩存的空間為20k,那么發送緩存可用空間為20-8=12k,大於請求發送的8k,所以send函數將數據做拷貝后,並立即返回8192;

假如發送緩存的空間為12k,那么此時發送緩存可用空間還有12-8=4k,send()會返回4096,應用程序發現返回的值小於請求發送的大小值后,可以認為緩存區已滿,這時必須阻塞(或通過select等待下一次socket可寫的信號),如果應用程序不理會,立即再次調用send,那么會得到-1的值, 在linux下表現為errno=EAGAIN.

C. 接收應用程序在處理完1k數據后,關閉了socket:
接收主機作為主動關閉者,連接將處於FIN_WAIT1的半關閉狀態(等待對方的ack).然后,發送應用程序會收到socket可讀的信號(通常是 select調用返回socket可讀),但在讀取時會發現recv函數返回0,這時應該調用close函數來關閉socket(發送給對方ack);

如果發送應用程序沒有處理這個可讀的信號,而是在send,那么這要分兩種情況來考慮,假如是在發送端收到RST標志之后調用send,send將返回-1,同時errno設為ECONNRESET表示對端網絡已斷開,但是,也有說法是進程會收到SIGPIPE信號,該信號的默認響應動作是退出進程,如果忽略該信號,那么send是返回-1,errno為EPIPE(未證實);如果是在發送端收到RST標志之前,則send像往常一樣工作;

以上說的是非阻塞的send情況,假如send是阻塞調用,並且正好處於阻塞時(例如一次性發送一個巨大的buf,超出了發送緩存),對端socket關閉,那么send將返回成功發送的字節數,如果再次調用send,那么會同上一樣.

D. 交換機或路由器的網絡斷開:
接收應用程序在處理完已收到的1k數據后,會繼續從緩存區讀取余下的1k數據,然后就表現為無數據可讀的現象,這種情況需要應用程序來處理超時.一般做法是設定一個select等待的最大時間,如果超出這個時間依然沒有數據可讀,則認為socket已不可用.

發送應用程序會不斷的將余下的數據發送到網絡上,但始終得不到確認,所以緩存區的可用空間持續為0,這種情況也需要應用程序來處理.

如果不由應用程序來處理這種情況超時的情況,也可以通過tcp協議本身來處理,具體可以查看sysctl項中的:
net.ipv4.tcp_keepalive_intvl
net.ipv4.tcp_keepalive_probes
net.ipv4.tcp_keepalive_time
 
 
///////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////
http://www.ixpub.net/thread-1446913-1-1.html
 
發送成功只是表示發到了內核socket緩沖區
此時如果close,正常情況會進入TIME_WAIT狀態,在此狀態,對端可以繼續接收數據
但是如果發送方的接收緩沖區還有未讀數據,就會走異常close的途徑,置RST,立刻結束連接,沒有TIME_WAIT狀態。這時對端就收不全數據,報錯: Connection reset by peer。
 
///////////////////////////////////////////////////////////////////////////////////////////
值得參考的 TCP send和recv函數解析
http://blog.csdn.net/wjtxt/article/details/6603456

一、 滑動窗口的概念

        TCP數據包的TCP頭部有一個window字段,它主要是用來告訴對方自己能接收多大的數據(注意只有TCP包中的數據部分占用這個空間),這個字段在通信雙方建立連接時協商確定,並且在通信過程中不斷更新,故取名為滑動窗口。有了這個字段,數據發送方就知道自己該不該發送數據,以及該發多少數據了。TCP協議的流量控制正是通過滑動窗口實現,從而保證通信雙方的接收緩沖區不會溢出,數據不會丟失。

由於窗口大小在TCP頭部只有16位來表示,所以它的最大值是65536,但是對於一些情況來說需要使用更大的滑動窗口,這時候就要使用擴展的滑動窗口,如光纖高速通信網絡,或者是衛星長連接網絡,需要窗口盡可能的大。這時會使用擴展的32位的滑動窗口大小。

二、 滑動窗口移動規則

        1、窗口合攏:在收到對端數據后,自己確認了數據的正確性,這些數據會被存儲到接收緩沖區,等待應用程序獲取。但這時候因為已經確認了數據的正確性,需要向對方發送確認響應ACK,又因為這些數據還沒有被應用進程取走,這時候便需要進行窗口合攏,緩沖區的窗口左邊緣向右滑動。注意響應的ACK序號是對方發送數據包的序號,一個對方發送的序號,可能因為窗口張開會被響應(ACK)多次。

        2、窗口張開:窗口收縮后,應用進程一旦從緩沖區(滑動窗口區或接收緩沖區)中取出數據,TCP的滑動窗口需要進行擴張,這時候窗口的右邊緣向右擴張,實際上窗口這是一個環形緩沖區,窗口的右邊緣擴張會使用原來被應用進程取走內容的緩沖區。在窗口進行擴張后,需要使用ACK通知對端,這時候ACK的序號依然是上次確認收到包的序號。

        3、窗口收縮,窗口的右邊緣向左滑動,稱為窗口收縮,HostRequirement RFC強烈建議不要這樣做,但TCP必須能夠在某一端產生這種情況時進行處理。

三、send行為

        默認情況下,send的功能是拷貝指定長度的數據到發送緩沖區,只有當數據被全部拷貝完成后函數才會正確返回,否則進入阻塞狀態或等待超時。如果你想修改這種默認行為,將數據直接發送到目標機器,可以將發送緩沖區大小設為0,這樣當send返回時,就表示數據已經正確的、完整的到達了目標機器。注意,這里只表示數據到達目標機器網絡緩沖區,並不表示數據已經被對方應用層接收了。

        協議層在數據發送過程中,根據對方的滑動窗口,再結合MSS值共同確定TCP報文中數據段的長度,以確保對方接收緩沖區不會溢出。當本方發送緩沖區尚有數據沒有發送,而對方滑動窗口已經為0時,協議層將啟動探測機制,即每隔一段時間向對方發送一個字節的數據,時間間隔會從剛開始的30s調整為1分鍾,最后穩定在2分鍾。這個探測機制不僅可以檢測到對方滑動窗口是否變化,同時也可以發現對方是否有異常退出的情況。

        push標志指示接收端應盡快將數據提交給應用層。如果send函數提交的待發送數據量較小,例如小於1460B(參照MSS值確定),那么協議層會將該報文中的TCP頭部的push字段置為1;如果待發送的數據量較大,需要拆成多個數據段發送時,協議層只會將最后一個分段報文的TCP頭部的push字段置1。

四、recv行為

        默認情況下,recv的功能是從接收緩沖區讀取(其實就是拷貝)指定長度的數據。如果將接收緩沖區大小設為0,recv將直接從協議緩沖區(滑動窗口區)讀取數據,避免了數據從協議緩沖區到接收緩沖區的拷貝。recv返回的條件有兩種:

      1. recv函數傳入的應用層接收緩沖區已經讀滿

      2. 協議層接收到push字段為1的TCP報文,此時recv返回值為實際接收的數據長度

        協議層收到TCP數據包后(保存在滑動窗口區),本方的滑動窗口合攏(窗口值減小);當協議層將數據拷貝到接收緩沖區(滑動窗口區—>接收緩沖區),或者應用層調用recv接收數據(接收緩沖區—>應用層緩沖區,滑動窗口區—>應用層緩沖區)后,本方的滑動窗口張開(窗口值增大)。收到數據更新window后,協議層向對方發送ACK確認。

        協議層的數據接收動作完全由發送動作驅動,是一個被動行為。在應用層沒有任何干涉行為的情況下(比如recv操作等),協議層能夠接收並保存的最大數據大小是窗口大小與接收緩沖區大小之和。Windows系統的窗口大小默認是64K,接收緩沖區默認為8K,所以默認情況下協議層最多能夠被動接收並保存72K的數據。

/////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////

http://blog.csdn.net/wjtxt/article/details/6598925

TCP連接關閉的問題:

 

 從TCP協議角度來看,一個已建立的TCP連接有兩種關閉方式,一種是正常關閉,即四次揮手關閉連接;還有一種則是異常關閉,我們通常稱之為連接重置(RESET)。

        首先說一下正常關閉時四次揮手的狀態變遷,關閉連接的主動方狀態變遷是FIN_WAIT_1->FIN_WAIT_2->TIME_WAIT,而關閉連接的被對方的狀態變遷是CLOSE_WAIT->LAST_ACK->TIME_WAIT。在四次揮手過程中ACK包都是協議棧自動完成的,而FIN包則必須由應用層通過closesocket或shutdown主動發送,通常連接正常關閉后,recv會得到返回值0,send會得到錯誤碼10058。

        除此之外,在我們的日常應用中,連接異常關閉的情況也很多。比如應用程序被強行關閉、本地網絡突然中斷(禁用網卡、網線拔出)、程序處理不當等都會導致連接重置,連接重置時將會產生RST包,同時網絡絡緩沖區中未接收(發送)的數據都將丟失。連接重置后,本方send或recv會得到錯誤碼10053(closesocket時是10038),對方recv會得到錯誤碼10054,send則得到錯誤碼10053(closesocket時是10054)。

        操作系統為我們提供了兩個函數來關閉一個TCP連接,分別是closesocket和shutdown。通常情況下,closesocket會向對方發送一個FIN包,但是也有例外。比如有一個工作線程正在調用recv接收數據,此時外部調用closesocket,會導致連接重置,同時向對方發送一個RST包,這個RST包是由本方主動產生的。

        shutdown可以用來關閉指定方向的連接,該函數接收兩個參數,一個是套接字,另一個是關閉的方向,可用值為SD_SEND,SD_RECEIVE和SD_BOTH。方向取值為SD_SEND時,無論socket處於什么狀態(recv阻塞,或空閑狀態),都會向對方發送一個FIN包,注意這點與closesocket的區別。此時本方進入FIN_WAIT_2狀態,對方進入CLOSE_WAIT狀態,本方依然可以調用recv接收數據;方向取值為SD_RECEIVE時,雙發連接狀態沒有改變,依然處於ESTABLISHED狀態,本方依然可以send數據,但是,如果對方再調用send方法,連接會被立即重置,同時向對方發送一個RST包,這個RST包是被動產生的,這點注意與closesocket的區別。


免責聲明!

本站轉載的文章為個人學習借鑒使用,本站對版權不負任何法律責任。如果侵犯了您的隱私權益,請聯系本站郵箱yoyou2525@163.com刪除。



 
粵ICP備18138465號   © 2018-2025 CODEPRJ.COM