非阻塞模式下 send 和 recv 函數的返回值總結

我們來根據前面的討論來總結一下 sendrecv 函數的各種返回值意義:

我們來逐一介紹下這三種情況:

  • 返回值大於 0

對於 sendrecv 函數返回值大於 0,表示發送或接收多少位元組,需要注意的是,在這種情形下,我們一定要判斷下 send 函數的返回值是不是我們期望發送的緩衝區長度,而不是簡單判斷其返回值大於 0。舉個例子:

int n = send(socket, buf, buf_length, 0);
if (n > 0)
{
printf("send data successfully
");
}

很多新手會寫出上述代碼,雖然返回值 n 大於 0,但是實際情形下,由於對端的 TCP 窗口可能因為缺少一部分位元組就滿了,所以返回值 n 的值可能在 (0, buf_length] 之間,當 0 < n < buf_length 時,雖然此時 send 函數是調用成功了,但是業務上並不算正確,因為有部分數據並沒發出去。你可能在一次測試中測不出 n 不等於 buf_length 的情況,但是不代表實際中不存在。所以,建議要麼認為返回值 n 等於 buf_length 才認為正確,要麼在一個循環中調用 send 函數,如果數據一次性發不完,記錄偏移量,下一次從偏移量處接著發,直到全部發送完為止。

//推薦的方式一
int n = send(socket, buf, buf_length, 0);
if (n == buf_length)
{
printf("send data successfully
");
}

//推薦的方式二:在一個循環裡面根據偏移量發送數據
bool SendData(const char* buf, int buf_length)
{
//已發送的位元組數目
int sent_bytes = 0;
int ret = 0;
while (true)
{
ret = send(m_hSocket, buf + sent_bytes, buf_length - sent_bytes, 0);
if (ret == -1)
{
if (errno == EWOULDBLOCK)
{
//嚴謹的做法,這裡如果發不出去,應該緩存尚未發出去的數據,後面介紹
break;
}
else if (errno == EINTR)
continue;
else
return false;
}
else if (ret == 0)
{
return false;
}

sent_bytes += ret;
if (sent_bytes == buf_length)
break;

//稍稍降低 CPU 的使用率
usleep(1);
}

return true;
}

?

  • 返回值等於 0

通常情況下,如果 send 或者 recv 函數返回 0,我們就認為對端關閉了連接,我們這端也關閉連接即可,這是實際開發時最常見的處理邏輯。

但是,現在還有一種情形就是,假設調用 send 函數傳遞的數據長度就是 0 呢?send 函數會是什麼行為?對端會 recv 到一個 0 位元組的數據嗎?需要強調的是,在實際開發中,你不應該讓你的程序有任何機會去 send 0 位元組的數據,這是一種不好的做法。 這裡僅僅用於實驗性討論,我們來通過一個例子,來看下 send 一個長度為 0 的數據,send 函數的返回值是什麼?對端會 recv0 位元組的數據嗎?

server 端代碼:

/**
* 驗證recv函數接受0位元組的行為,server端,server_recv_zero_bytes.cpp
* zhangyl 2018.12.17
*/
#include <sys/types.h>
#include <sys/socket.h>
#include <arpa/inet.h>
#include <unistd.h>
#include <iostream>
#include <string.h>
#include <vector>

int main(int argc, char* argv[])
{
//1.創建一個偵聽socket
int listenfd = socket(AF_INET, SOCK_STREAM, 0);
if (listenfd == -1)
{
std::cout << "create listen socket error." << std::endl;
return -1;
}

//2.初始化伺服器地址
struct sockaddr_in bindaddr;
bindaddr.sin_family = AF_INET;
bindaddr.sin_addr.s_addr = htonl(INADDR_ANY);
bindaddr.sin_port = htons(3000);
if (bind(listenfd, (struct sockaddr *)&bindaddr, sizeof(bindaddr)) == -1)
{
std::cout << "bind listen socket error." << std::endl;
close(listenfd);
return -1;
}

//3.啟動偵聽
if (listen(listenfd, SOMAXCONN) == -1)
{
std::cout << "listen error." << std::endl;
close(listenfd);
return -1;
}

int clientfd;

struct sockaddr_in clientaddr;
socklen_t clientaddrlen = sizeof(clientaddr);
//4. 接受客戶端連接
clientfd = accept(listenfd, (struct sockaddr *)&clientaddr, &clientaddrlen);
if (clientfd != -1)
{
while (true)
{
char recvBuf[32] = {0};
//5. 從客戶端接受數據,客戶端沒有數據來的時候會在 recv 函數處阻塞
int ret = recv(clientfd, recvBuf, 32, 0);
if (ret > 0)
{
std::cout << "recv data from client, data: " << recvBuf << std::endl;
}
else if (ret == 0)
{
std::cout << "recv 0 byte data." << std::endl;
continue;
}
else
{
//出錯
std::cout << "recv data error." << std::endl;
break;
}
}
}

//關閉客戶端socket
close(clientfd);
//7.關閉偵聽socket
close(listenfd);

return 0;
}

上述代碼偵聽埠號是 3000,代碼 55 行調用了 recv 函數,如果客戶端一直沒有數據,程序會阻塞在這裡。

client 端代碼:

/**
* 驗證非阻塞模式下send函數發送0位元組的行為,client端,nonblocking_client_send_zero_bytes.cpp
* zhangyl 2018.12.17
*/
#include <sys/types.h>
#include <sys/socket.h>
#include <arpa/inet.h>
#include <unistd.h>
#include <iostream>
#include <string.h>
#include <stdio.h>
#include <fcntl.h>
#include <errno.h>

#define SERVER_ADDRESS "127.0.0.1"
#define SERVER_PORT 3000
#define SEND_DATA ""

int main(int argc, char* argv[])
{
//1.創建一個socket
int clientfd = socket(AF_INET, SOCK_STREAM, 0);
if (clientfd == -1)
{
std::cout << "create client socket error." << std::endl;
return -1;
}

//2.連接伺服器
struct sockaddr_in serveraddr;
serveraddr.sin_family = AF_INET;
serveraddr.sin_addr.s_addr = inet_addr(SERVER_ADDRESS);
serveraddr.sin_port = htons(SERVER_PORT);
if (connect(clientfd, (struct sockaddr *)&serveraddr, sizeof(serveraddr)) == -1)
{
std::cout << "connect socket error." << std::endl;
close(clientfd);
return -1;
}

//連接成功以後,我們再將 clientfd 設置成非阻塞模式,
//不能在創建時就設置,這樣會影響到 connect 函數的行為
int oldSocketFlag = fcntl(clientfd, F_GETFL, 0);
int newSocketFlag = oldSocketFlag | O_NONBLOCK;
if (fcntl(clientfd, F_SETFL, newSocketFlag) == -1)
{
close(clientfd);
std::cout << "set socket to nonblock error." << std::endl;
return -1;
}

//3. 不斷向伺服器發送數據,或者出錯退出
int count = 0;
while (true)
{
//發送 0 位元組的數據
int ret = send(clientfd, SEND_DATA, 0, 0);
if (ret == -1)
{
//非阻塞模式下send函數由於TCP窗口太小發不出去數據,錯誤碼是EWOULDBLOCK
if (errno == EWOULDBLOCK)
{
std::cout << "send data error as TCP Window size is too small." << std::endl;
continue;
}
else if (errno == EINTR)
{
//如果被信號中斷,我們繼續重試
std::cout << "sending data interrupted by signal." << std::endl;
continue;
}
else
{
std::cout << "send data error." << std::endl;
break;
}
}
else if (ret == 0)
{
//對端關閉了連接,我們也關閉
std::cout << "send 0 byte data." << std::endl;
}
else
{
count ++;
std::cout << "send data successfully, count = " << count << std::endl;
}

//每三秒發一次
sleep(3);
}

//5. 關閉socket
close(clientfd);

return 0;
}

client 端連接伺服器成功以後,每隔 3 秒調用 send 一次發送一個 0 位元組的數據。除了先啟動 server 以外,我們使用 tcpdump 抓一下經過埠 3000 上的數據包,使用如下命令:

tcpdump -i any tcp port 3000

然後啟動 client ,我們看下結果:

客戶端確實是每隔 3 秒 send 一次數據。此時我們使用 lsof -i -Pn 命令查看連接狀態,也是正常的:

然後,tcpdump 抓包結果輸出中,除了連接時的三次握手數據包,再也無其他數據包,也就是說,send 函數發送 0 位元組數據,client 的協議棧並不會把這些數據發出去。

[root@localhost ~]# tcpdump -i any tcp port 3000
tcpdump: verbose output suppressed, use -v or -vv for full protocol decode
listening on any, link-type LINUX_SLL (Linux cooked), capture size 262144 bytes
17:37:03.028449 IP localhost.48820 > localhost.hbci: Flags [S], seq 1632283330, win 43690, options [mss 65495,sackOK,TS val 201295556 ecr 0,nop,wscale 7], length 0
17:37:03.028479 IP localhost.hbci > localhost.48820: Flags [S.], seq 3669336158, ack 1632283331, win 43690, options [mss 65495,sackOK,TS val 201295556 ecr 201295556,nop,wscale 7], length 0
17:37:03.028488 IP localhost.48820 > localhost.hbci: Flags [.], ack 1, win 342, options [nop,nop,TS val 201295556 ecr 201295556], length 0

因此,server 端也會一直沒有輸出,如果你用的是 gdb 啟動 server,此時中斷下來會發現,server 端由於沒有數據會一直阻塞在 recv 函數調用處(55 行)。

上述示例再次驗證了,send 一個 0 位元組的數據沒有任何意思,希望讀者在實際開發時,避免寫出這樣的代碼。


本文首發於『easyserverdev』公眾號,歡迎關注,轉載請保留版權信息。

歡迎加入高性能伺服器開發 QQ 羣一起交流: 578019391 。


推薦閱讀:
相關文章