有關send()和recv()函數的理解

本文轉載自 http://blog.chinaunix.net/space.php?uid=11140746&do=blog&id=2903926

int send( SOCKET s,      const char FAR *buf,      int len,      int flags );  

不論是客戶還是服務器應用程序都用send函數來向TCP連接的另一端發送數據。

客戶程序一般用send函數向服務器發送請求,而服務器則通常用send函數來向客戶程序發送應答。

該函數的第一個參數指定發送端套接字描述符;

第二個參數指明一個存放應用程序要發送數據的緩衝區;

第三個參數指明實際要發送的數據的字節數;

第四個參數一般置0。

這裏只描述同步Socket的send函數的執行流程。當調用該函數時,send先比較待發送數據的長度len和套接字s的發送緩衝的 長度, 如果len大於s的發送緩衝區的長度,該函數返回SOCKET_ERROR;如果len小於或者等於s的發送緩衝區的長度,那麼send先檢查協議 是否正在發送s的發送緩衝中的數據,如果是就等待協議把數據發送完,如果協議還沒有開始發送s的發送緩衝中的數據或者s的發送緩衝中沒有數據,那麼 send就比較s的發送緩衝區的剩餘空間和len,如果len大於剩餘空間大小send就一直等待協議把s的發送緩衝中的數據發送完,如果len小於剩餘 空間大小send就僅僅把buf中的數據copy到剩餘空間裏(注意並不是send把s的發送緩衝中的數據傳到連接的另一端的,而是協議傳的,send僅僅是把buf中的數據copy到s的發送緩衝區的剩餘空間裏)。如果send函數copy數據成功,就返回實際copy的字節數,如果send在copy數據時出現錯誤,那麼send就返回SOCKET_ERROR;如果send在等待協議傳送數據時網絡斷開的話,那麼send函數也返回SOCKET_ERROR。

要注意send函數把buf中的數據成功copy到s的發送緩衝的剩餘空間裏後它就返回了,但是此時這些數據並不一定馬上被傳到連接的另一端如 果協議在後續的傳送過程中出現網絡錯誤的話,那麼下一個Socket函數就會返回SOCKET_ERROR。(每一個除send外的Socket函數在執 行的最開始總要先等待套接字的發送緩衝中的數據被協議傳送完畢才能繼續,如果在等待時出現網絡錯誤,那麼該Socket函數就返回 SOCKET_ERROR)

注意:在Unix系統下,如果send在等待協議傳送數據時網絡斷開的話,調用send的進程會接收到一個SIGPIPE信號,進程對該信號的默認處理是進程終止。

recv函數

int recv( SOCKET s,     char FAR *buf,      int len,     int flags     );   

不論是客戶還是服務器應用程序都用recv函數從TCP連接的另一端接收數據。

該函數的第一個參數指定接收端套接字描述符;

第二個參數指明一個緩衝區,該緩衝區用來存放recv函數接收到的數據;

第三個參數指明buf的長度;

第四個參數一般置0。

這裏只描述同步Socket的recv函數的執行流程。當應用程序調用recv函數時,recv先等待s的發送緩衝中的數據被協議傳送完畢,如果協議在傳送s的發送緩衝中的數據時出現網絡錯誤,那麼recv函數返回SOCKET_ERROR,如果s的發送緩衝中沒有數 據或者數據被協議成功發送完畢後,recv先檢查套接字s的接收緩衝區,如果s接收緩衝區中沒有數據或者協議正在接收數據,那麼recv就一直等待,只到 協議把數據接收完畢。當協議把數據接收完畢,recv函數就把s的接收緩衝中的數據copy到buf中(注意協議接收到的數據可能大於buf的長度,所以 在這種情況下要調用幾次recv函數才能把s的接收緩衝中的數據copy完。recv函數僅僅是copy數據,真正的接收數據是協議來完成的),recv函數返回其實際copy的字節數。如果recv在copy時出錯,那麼它返回SOCKET_ERROR;如果recv函數在等待協議接收數據時網絡中斷了,那麼它返回0。

注意:在Unix系統下,如果recv函數在等待協議接收數據時網絡斷開了,那麼調用recv的進程會接收到一個SIGPIPE信號,進程對該信號的默認處理是進程終止。
 

cp協議本身是可靠的,並不等於應用程序用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_defaultnet.core.wmem_max的值來分配內存的.

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

view plainprint?

1.       socklen_t sendbuflen = 0;  

2.       socklen_t len = sizeof(sendbuflen);  

3.       getsockopt(clientSocket, SOL_SOCKET, SO_SNDBUF, (void*)&sendbuflen, &len);  

4.       printf("default,sendbuf:%d\n", sendbuflen);  

5.         

6.       sendbuflen = 10240;  

7.       setsockopt(clientSocket, SOL_SOCKET, SO_SNDBUF, (void*)&sendbuflen, len);  

8.       getsockopt(clientSocket, SOL_SOCKET, SO_SNDBUF, (void*)&sendbuflen, &len);  

9.       printf("now,sendbuf:%d\n", sendbuflen);  

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,errnoEPIPE(未證實);如果是在發送端收到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

 

 

 

send函數特點及相關問題收藏

在send函數的help裏面看到

The successful completion of a send call does not indicate that the data was successfully delivered.
send成功完成並不代表數據已經成功送達。

If no buffer space is available within the transport system to hold the data to be transmitted, send will block unless the socket has been placed in nonblocking mode. 
如果沒有緩衝存儲待發送的數據,send會阻塞直到socket被設置爲非阻塞模式,

On nonblocking stream-oriented sockets, the number of bytes written can be between 1 and the requested length, depending on buffer availability on both client and server machines. 
在非阻塞流模式socket中,寫入的字節可以是1到需要的長度,依賴於客戶端和服務器的緩衝。
The select or WSAEventSelect function can be used to determine when it is possible to send more data.
select 
或 WSAEventSelect 函數可以用於決定什麼時候可以繼續發送數據


阻塞模式下send並不是說直到你發送數據到對方機器才返回的意思,它是說把你要發送的數據放入發送緩衝後,就直接返回。而不是阻塞時,如發送緩衝區沒有了,他就直接返回,而阻塞時會等待發送緩衝區有空間。


先看看在阻塞模式下send的表現吧(注意緩衝區的大小,我這裏是16k)
1,發送一個小於16k的數據,send馬上就返回了
      也就說是,send把待發送的數據放入發送緩衝馬上就返回了,前提是發送的數據字節數小於緩衝大小
2,發送一個大於16k的數據,send沒有馬上返回,阻塞了一下
      send一定要把所有數據放入緩衝區纔會返回,假設我們發32k的數據,當send返回的時候,有16k數據已經到達另一端,剩下16k還在緩衝裏面沒有發出去

在阻塞模式下
如果發送成功,返回的nBytes一定等於len
        nBytes = send(m_socket,buf,len,0);
也就是在上面代碼中那個發送循環其實是沒有必要的

再看看在非阻塞模式下的情況吧
1,發送一個小於16k的數據,send馬上返回了,而且返回的字節長度是等於發送的字節長度的,情況和阻塞模式是向相同的

2,發送一個大於16k的數據,send也是馬上就返回了,返回的nByte小於待發送的字節數
      來模擬一下實際情況,假設我們有32k的數據要發送,

      第一次send,返回16384字節(16k),也就是填滿了緩衝區
      第二次send,在這之前sleep了1000毫秒,這段時間可能已經有5000字節從緩衝區發出,到達另外一端了,於是緩衝區空了5000字節出來,相應的,這次返回的是5000,表示新放入了5000字節到緩衝區
      第三次send  ,和第二次相同,又放了6000字節
      最後一次send,放入了剩下的字節數,這個時候緩衝還是有數據的。

再發送大於16k數據的情況下,那個send發送循環就是必須的了

發表評論
所有評論
還沒有人評論,想成為第一個評論的人麼? 請在上方評論欄輸入並且點擊發布.
相關文章