我們來根據前面的討論來總結一下 send 和 recv 函數的各種返回值意義:
我們來逐一介紹下這三種情況:
對於 send 和 recv 函數返回值大於 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; }
?
通常情況下,如果 send 或者 recv 函數返回 0,我們就認為對端關閉了連接,我們這端也關閉連接即可,這是實際開發時最常見的處理邏輯。
但是,現在還有一種情形就是,假設調用 send 函數傳遞的數據長度就是 0 呢?send 函數會是什麼行為?對端會 recv 到一個 0 位元組的數據嗎?需要強調的是,在實際開發中,你不應該讓你的程序有任何機會去 send 0 位元組的數據,這是一種不好的做法。 這裡僅僅用於實驗性討論,我們來通過一個例子,來看下 send 一個長度為 0 的數據,send 函數的返回值是什麼?對端會 recv 到 0 位元組的數據嗎?
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);
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 。