1. 程式人生 > >socket程式設計中send recv阻塞和非阻塞詳解

socket程式設計中send recv阻塞和非阻塞詳解

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

不論是客戶還是伺服器應用程式都用send函式來向TCP連線的另一端傳送資料。客戶程式一般用send函式向伺服器傳送請求,而伺服器則通常用send函式來向客戶程式傳送應答。

該函式的第一個引數指定傳送端套接字描述符;

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

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

第四個引數一般置0。

這裡只描述同步Socket的send函式的執行流程。當呼叫該函式時,

(1)send先比較待發送資料的長度len和套接字s的傳送緩衝的長度, 如果len大於s的傳送緩衝區的長度,該函式返回SOCKET_ERROR;

(2)如果len小於或者等於s的傳送緩衝區的長度,那麼send先檢查協議是否正在傳送s的傳送緩衝中的資料,如果是就等待協議把資料傳送完,如果協議 還沒有開始傳送s的傳送緩衝中的資料或者s的傳送緩衝中沒有資料,那麼send就比較s的傳送緩衝區的剩餘空間和len

(3)如果len大於剩餘空間大小,send就一直等待協議把s的傳送緩衝中的資料傳送完

(4)如果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訊號,程序對該訊號的預設處理是程序終止。

通過測試發現,非同步socket的send函式在網路剛剛斷開時還能傳送返回相應的位元組數,同時使用select檢測也是可寫的,但是過幾秒鐘之後,再send就會出錯了,返回-1。select也不能檢測出可寫了。

2. recv函式

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

不論是客戶還是伺服器應用程式都用recv函式從TCP連線的另一端接收資料。該函式的第一個引數指定接收端套接字描述符;

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

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

第四個引數一般置0。

這裡只描述同步Socket的recv函式的執行流程。當應用程式呼叫recv函式時,

(1)recv先等待s的傳送緩衝中的資料被協議傳送完畢,如果協議在傳送s的傳送緩衝中的資料時出現網路錯誤,那麼recv函式返回SOCKET_ERROR,

(2)如果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訊號,程序對該訊號的預設處理是程序終止。

阻塞就是幹不完不準回來,   
非組賽就是你先幹,我現看看有其他事沒有,完了告訴我一聲

我們拿最常用的send和recv兩個函式來說吧... 
比如你呼叫send函式傳送一定的Byte,在系統內部send做的工作其實只是把資料傳輸(Copy)到TCP/IP協議棧的輸出緩衝區,它執行成功並不代表資料已經成功的傳送出去了,如果TCP/IP協議棧沒有足夠的可用緩衝區來儲存你Copy過來的資料的話...這時候就體現出阻塞和非阻塞的不同之處了:對於阻塞模式的socket send函式將不返回直到系統緩衝區有足夠的空間把你要傳送的資料Copy過去以後才返回,而對於非阻塞的socket來說send會立即返回WSAEWOULDDBLOCK告訴呼叫者說:"傳送操作被阻塞了!!!你想辦法處理吧..." 
對於recv函式,同樣道理,該函式的內部工作機制其實是在等待TCP/IP協議棧的接收緩衝區通知它說:嗨,你的資料來了.對於阻塞模式的socket來說如果TCP/IP協議棧的接收緩衝區沒有通知一個結果給它它就一直不返回:耗費著系統資源....對於非阻塞模式的socket該函式會馬上返回,然後告訴你:WSAEWOULDDBLOCK---"現在沒有資料,回頭在來看看"

讀資料的時候需要考慮的是當recv()返回的大小如果等於請求的大小,那麼很有可能是緩衝區還有資料未讀完,也意味著該次事件還沒有處理完,所以還需要再次讀取
while(rs)
{
buflen = recv(activeevents[i].data.fd, buf, sizeof(buf), 0);
if(buflen < 0)
{
    // 由於是非阻塞的模式,所以當errno為EAGAIN時,表示當前緩衝區已無資料可讀
    // 在這裡就當作是該次事件已處理處.
    if(errno == EAGAIN)
     break;
    else
     return;
   }
   else if(buflen == 0)
   {
     // 這裡表示對端的socket已正常關閉.
   }
   if(buflen == sizeof(buf)
     rs = 1;   // 
需要再次讀取
   else
     rs = 0;
}


還有,假如傳送端流量大於接收端的流量(意思是epoll所在的程式讀比轉發的socket要快),由於是非阻塞的socket,那麼send()函式雖然返回,但實際緩衝區的資料並未真正發給接收端,這樣不斷的讀和發,當緩衝區滿後會產生EAGAIN錯誤(參考man send),同時,不理會這次請求傳送的資料.所以,需要封裝socket_send()的函式用來處理這種情況,該函式會盡量將資料寫完再返回,返回-1表示出錯。在socket_send()內部,當寫緩衝已滿(send()返回-1,errnoEAGAIN),那麼會等待後再重試.這種方式並不很完美,在理論上可能會長時間的阻塞在socket_send()內部,但暫沒有更好的辦法.

ssize_t socket_send(int sockfd, const char* buffer, size_t buflen)
{
ssize_t tmp;
size_t total = buflen;
const char *p = buffer;

while(1)
{
    tmp = send(sockfd, p, total, 0);
    if(tmp < 0)
    {
      // 當send收到訊號時,可以繼續寫,但這裡返回-1.
      if(errno == EINTR)
        return -1;

      // 當socket是非阻塞時,如返回此錯誤,表示寫緩衝佇列已滿,
      // 在這裡做延時後再重試.
      if(errno == EAGAIN)
      {
        usleep(1000);
        continue;
      }

      return -1;
    }

    if((size_t)tmp == total)
      return buflen;

    total -= tmp;
    p += tmp;
}

return tmp;
}