1. 程式人生 > >Socket中send()函式和recv()函式詳解

Socket中send()函式和recv()函式詳解

1、send函式

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

不論是客戶還是伺服器應用程式都用send函式來向TCP連線的另一端傳送資料。

客戶程式一般用send函式向伺服器傳送請求,而伺服器則通常用send函式來向客戶程式傳送應答。

(1)第一個引數指定傳送端套接字描述符;

(2)第二個引數指明一個存放應用程式要傳送資料的緩衝區;

(3)第三個引數指明實際要傳送的資料的位元組數;

(4)第四個引數一般置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訊號,程序對該訊號的預設處理是程序終止。

2、recv函式

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

不論是客戶還是伺服器應用程式都用recv函式從TCP連線的另一端接收資料。

(1)第一個引數指定接收端套接字描述符;

(2)第二個引數指明一個緩衝區,該緩衝區用來存放recv函式接收到的資料;

(3)第三個引數指明buf的長度;

(4)第四個引數一般置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訊號,程序對該訊號的預設處理是程序終止。

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

在阻塞模式下, send函式的過程是將應用程式請求傳送的資料拷貝到傳送快取中傳送並得到確認後再返回.但由於傳送快取的存在,表現為:如果傳送快取大小比請求傳送的大小要大,那麼send函式立即返回,同時向網路中傳送資料;否則,send向網路傳送快取中不能容納的那部分資料,並等待對端確認後再返回(接收端只要將資料收到接收快取中,就會確認,並不一定要等待應用程式呼叫recv);

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

3、快取大小檢視即更改

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的值來分配記憶體的.

應用程式應該根據應用的特性在程式中更改傳送快取大小:

  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. sendbuflen = 10240;  
  6. setsockopt(clientSocket, SOL_SOCKET, SO_SNDBUF, (void*)&sendbuflen, len);  
  7. getsockopt(clientSocket, SOL_SOCKET, SO_SNDBUF, (void*)&sendbuflen, &len);  
  8. printf("now,sendbuf:%d\n", sendbuflen);  
需要注意的是,雖然將傳送快取設定成了10k,但實際上,協議棧會將其擴大1倍,設為20k.

4、例項分析

在實際應用中,如果傳送端是非阻塞傳送,由於網路的阻塞或者接收端處理過慢,通常出現的情況是,傳送應用程式看起來發送了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

5、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傳送迴圈就是必須的了