我们来根据前面的讨论来总结一下 send 和 recv 函数的各种返回值意义:
返回值 n | 返回值含义 |
---|---|
大于 0 | 成功发送 n 个字节 |
0 | 对端关闭连接 |
小于 0( -1) | 出错或者被信号中断或者对端 TCP 窗口太小数据发不出去(send)或者当前网卡缓冲区已无数据可收(recv) |
我们来逐一介绍下这三种情况:
1 //推荐的方式一 2 int n = send(socket, buf, buf_length, 0); 3 if (n == buf_length) 4 { 5 printf("send data successfully\n"); 6 }
1//推荐的方式二:在一个循环里面根据偏移量发送数据 2bool SendData(const char* buf , int buf_length) 3{ 4 //已发送的字节数目 5 int sent_bytes = 0; 6 int ret = 0; 7 while (true) 8 { 9 ret = send(m_hSocket, buf + sent_bytes, buf_length - sent_bytes, 0); 10 if (nRet == -1) 11 { 12 if (errno == EWOULDBLOCK) 13 { 14 //严谨的做法,这里如果发不出去,应该缓存尚未发出去的数据,后面介绍 15 break; 16 } 17 else if (errno == EINTR) 18 continue; 19 else 20 return false; 21 } 22 else if (nRet == 0) 23 { 24 return false; 25 } 26 27 sent_bytes += ret; 28 if (sent_bytes == buf_length) 29 break; 30 31 //稍稍降低 CPU 的使用率 32 usleep(1); 33 } 34 35 return true; 36}
1 /** 2 * 验证recv函数接受0字节的行为,server端,server_recv_zero_bytes.cpp 3 * zhangyl 2018.12.17 4 */ 5 #include <sys/types.h> 6 #include <sys/socket.h> 7 #include <arpa/inet.h> 8 #include <unistd.h> 9 #include <iostream> 10 #include <string.h> 11 #include <vector> 12 13 int main(int argc, char* argv[]) 14 { 15 //1.创建一个侦听socket 16 int listenfd = socket(AF_INET, SOCK_STREAM, 0); 17 if (listenfd == -1) 18 { 19 std::cout << "create listen socket error." << std::endl; 20 return -1; 21 } 22 23 //2.初始化服务器地址 24 struct sockaddr_in bindaddr; 25 bindaddr.sin_family = AF_INET; 26 bindaddr.sin_addr.s_addr = htonl(INADDR_ANY); 27 bindaddr.sin_port = htons(3000); 28 if (bind(listenfd, (struct sockaddr *)&bindaddr, sizeof(bindaddr)) == -1) 29 { 30 std::cout << "bind listen socket error." << std::endl; 31 close(listenfd); 32 return -1; 33 } 34 35 //3.启动侦听 36 if (listen(listenfd, SOMAXCONN) == -1) 37 { 38 std::cout << "listen error." << std::endl; 39 close(listenfd); 40 return -1; 41 } 42 43 int clientfd; 44 45 struct sockaddr_in clientaddr; 46 socklen_t clientaddrlen = sizeof(clientaddr); 47 //4. 接受客户端连接 48 clientfd = accept(listenfd, (struct sockaddr *)&clientaddr, &clientaddrlen); 49 if (clientfd != -1) 50 { 51 while (true) 52 { 53 char recvBuf[32] = {0}; 54 //5. 从客户端接受数据,客户端没有数据来的时候会在recv函数处阻塞 55 int ret = recv(clientfd, recvBuf, 32, 0); 56 if (ret > 0) 57 { 58 std::cout << "recv data from client, data: " << recvBuf << std::endl; 59 } 60 else if (ret == 0) 61 { 62 std::cout << "recv 0 byte data." << std::endl; 63 continue; 64 } 65 else 66 { 67 //出错 68 std::cout << "recv data error." << std::endl; 69 break; 70 } 71 } 72 } 73 74 75 //关闭客户端socket 76 close(clientfd); 77 //7.关闭侦听socket 78 close(listenfd); 79 80 return 0; 81 }
上述代码侦听端口号是 3000,代码 55 行调用了 recv 函数,如果客户端一直没有数据,程序会阻塞在这里。
client 端代码:
1/** 2 * 验证非阻塞模式下send函数发送0字节的行为,client端,nonblocking_client_send_zero_bytes.cpp 3 * zhangyl 2018.12.17 4 */ 5#include <sys/types.h> 6#include <sys/socket.h> 7#include <arpa/inet.h> 8#include <unistd.h> 9#include <iostream> 10#include <string.h> 11#include <stdio.h> 12#include <fcntl.h> 13#include <errno.h> 14 15#define SERVER_ADDRESS "127.0.0.1" 16#define SERVER_PORT 3000 17#define SEND_DATA "" 18 19int main(int argc, char* argv[]) 20{ 21 //1.创建一个socket 22 int clientfd = socket(AF_INET, SOCK_STREAM, 0); 23 if (clientfd == -1) 24 { 25 std::cout << "create client socket error." << std::endl; 26 return -1; 27 } 28 29 //2.连接服务器 30 struct sockaddr_in serveraddr; 31 serveraddr.sin_family = AF_INET; 32 serveraddr.sin_addr.s_addr = inet_addr(SERVER_ADDRESS); 33 serveraddr.sin_port = htons(SERVER_PORT); 34 if (connect(clientfd, (struct sockaddr *)&serveraddr, sizeof(serveraddr)) == -1) 35 { 36 std::cout << "connect socket error." << std::endl; 37 close(clientfd); 38 return -1; 39 } 40 41 //连接成功以后,我们再将 clientfd 设置成非阻塞模式, 42 //不能在创建时就设置,这样会影响到 connect 函数的行为 43 int oldSocketFlag = fcntl(clientfd, F_GETFL, 0); 44 int newSocketFlag = oldSocketFlag | O_NONBLOCK; 45 if (fcntl(clientfd, F_SETFL, newSocketFlag) == -1) 46 { 47 close(clientfd); 48 std::cout << "set socket to nonblock error." << std::endl; 49 return -1; 50 } 51 52 //3. 不断向服务器发送数据,或者出错退出 53 int count = 0; 54 while (true) 55 { 56 //发送 0 字节的数据 57 int ret = send(clientfd, SEND_DATA, 0, 0); 58 if (ret == -1) 59 { 60 //非阻塞模式下send函数由于TCP窗口太小发不出去数据,错误码是EWOULDBLOCK 61 if (errno == EWOULDBLOCK) 62 { 63 std::cout << "send data error as TCP Window size is too small." << std::endl; 64 continue; 65 } 66 else if (errno == EINTR) 67 { 68 //如果被信号中断,我们继续重试 69 std::cout << "sending data interrupted by signal." << std::endl; 70 continue; 71 } 72 else 73 { 74 std::cout << "send data error." << std::endl; 75 break; 76 } 77 } 78 else if (ret == 0) 79 { 80 //对端关闭了连接,我们也关闭 81 std::cout << "send 0 byte data." << std::endl; 82 } 83 else 84 { 85 count ++; 86 std::cout << "send data successfully, count = " << count << std::endl; 87 } 88 89 //每三秒发一次 90 sleep(3); 91 } 92 93 //5. 关闭socket 94 close(clientfd); 95 96 return 0; 97}
client 端连接服务器成功以后,每隔 3 秒调用 send 一次发送一个 0 字节的数据。除了先启动 server 以外,我们使用 tcpdump 抓一下经过端口 3000 上的数据包,使用如下命令:
1tcpdump -i any 'tcp port 3000'
然后启动 client ,我们看下结果:
客户端确实是每隔 3 秒 send 一次数据。此时我们使用 lsof -i -Pn 命令查看连接状态,也是正常的:
然后,tcpdump 抓包结果输出中,除了连接时的三次握手数据包,再也无其他数据包,也就是说,send 函数发送 0 字节数据,client 的协议栈并不会把这些数据发出去。
1[root@localhost ~]# tcpdump -i any 'tcp port 3000' 2tcpdump: verbose output suppressed, use -v or -vv for full protocol decode 3listening on any, link-type LINUX_SLL (Linux cooked), capture size 262144 bytes 417: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 517: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 617: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 字节的数据没有任何意思,希望读者在实际开发时,避免写出这样的代码。
本文分享自微信公众号 - 高性能服务器开发(transfer_3561453275)
原文出处及转载信息见文内详细说明,如有侵权,请联系 yunjia_community@tencent.com 删除。
原始发表时间:2019-03-14
本文参与腾讯云自媒体分享计划,欢迎正在阅读的你也加入,一起分享。