CppGuide社区 CppGuide社区
首页
  • 🔥最新谷歌C++风格指南(含C++17/20)
  • 🔥C++17详解
  • 🔥C++20完全指南
  • 🔥C++23快速入门
  • C++语言面试问题集锦
  • 🔥交易系统开发岗位求职与面试指南 (opens new window)
  • 第1章 高频C++11重难点知识解析
  • 第2章 Linux GDB高级调试指南
  • 第3章 C++多线程编程从入门到进阶
  • 第4章 C++网络编程重难点解析
  • 第5章 网络通信故障排查常用命令
  • 第6章 高性能网络通信协议设计精要
  • 第7章 高性能服务结构设计
  • 第8章 Redis网络通信模块源码分析
  • 第9章 后端服务重要模块设计探索
  • 🚀 全部章节.pdf 下载 (opens new window)
  • 源码分析系列

    • leveldb源码分析
    • libevent源码分析
    • Memcached源码分析
    • TeamTalk源码分析
    • 优质源码分享 (opens new window)
    • 🔥远程控制软件gh0st源码分析
  • 从零手写C++项目系列

    • 🔥C++游戏编程入门(零基础学C++)
    • 🔥使用C++17从零开发一个调试器 (opens new window)
    • 🔥使用C++20从零构建一个完整的低延迟交易系统 (opens new window)
    • 🔥使用C++从零写一个C语言编译器 (opens new window)
    • 🔥从零用C语言写一个Redis
  • 🔥Windows 10系统编程
  • 🔥Linux 5.x内核开发与调试 完全指南 (opens new window)
  • TCP源码实现超详细注释版.pdf (opens new window)
  • Go语言特性

    • Go系统接口编程
    • 高效Go并发编程
    • Go性能调优
    • Go项目架构设计
  • Go项目实战

    • 🔥使用Go从零开发一个数据库
    • 🔥使用Go从零开发一个编译器 (opens new window)
    • 🔥使用Go从零开发一个解释器 (opens new window)
    • 🔥使用Go从零开发一个解释器 (opens new window)
    • 🔥用Go从零写一个编排器(类Kubernetes) (opens new window)
Rust编程指南
  • SQL零基础指南
  • MySQL开发与调试指南
GitHub (opens new window)
首页
  • 🔥最新谷歌C++风格指南(含C++17/20)
  • 🔥C++17详解
  • 🔥C++20完全指南
  • 🔥C++23快速入门
  • C++语言面试问题集锦
  • 🔥交易系统开发岗位求职与面试指南 (opens new window)
  • 第1章 高频C++11重难点知识解析
  • 第2章 Linux GDB高级调试指南
  • 第3章 C++多线程编程从入门到进阶
  • 第4章 C++网络编程重难点解析
  • 第5章 网络通信故障排查常用命令
  • 第6章 高性能网络通信协议设计精要
  • 第7章 高性能服务结构设计
  • 第8章 Redis网络通信模块源码分析
  • 第9章 后端服务重要模块设计探索
  • 🚀 全部章节.pdf 下载 (opens new window)
  • 源码分析系列

    • leveldb源码分析
    • libevent源码分析
    • Memcached源码分析
    • TeamTalk源码分析
    • 优质源码分享 (opens new window)
    • 🔥远程控制软件gh0st源码分析
  • 从零手写C++项目系列

    • 🔥C++游戏编程入门(零基础学C++)
    • 🔥使用C++17从零开发一个调试器 (opens new window)
    • 🔥使用C++20从零构建一个完整的低延迟交易系统 (opens new window)
    • 🔥使用C++从零写一个C语言编译器 (opens new window)
    • 🔥从零用C语言写一个Redis
  • 🔥Windows 10系统编程
  • 🔥Linux 5.x内核开发与调试 完全指南 (opens new window)
  • TCP源码实现超详细注释版.pdf (opens new window)
  • Go语言特性

    • Go系统接口编程
    • 高效Go并发编程
    • Go性能调优
    • Go项目架构设计
  • Go项目实战

    • 🔥使用Go从零开发一个数据库
    • 🔥使用Go从零开发一个编译器 (opens new window)
    • 🔥使用Go从零开发一个解释器 (opens new window)
    • 🔥使用Go从零开发一个解释器 (opens new window)
    • 🔥用Go从零写一个编排器(类Kubernetes) (opens new window)
Rust编程指南
  • SQL零基础指南
  • MySQL开发与调试指南
GitHub (opens new window)
  • 第1章高频C++11重难点知识解析

  • 第2章Linux GDB高级调试指南

  • 第3章C++多线程编程从入门到进阶

  • 第4章C++网络编程重难点解析

    • 4.1 学习网络编程,你应该掌握哪些 socket 函数
    • 4.2 TCP 网络通信的基本流程
    • 4.3 设计跨平台网络通信库时需要注意的一些 socket 函数用法
    • 4.4 bind 函数重难点分析
    • 4.5 select 函数用法和原理
    • 4.6 socket 的阻塞模式和非阻塞模式
    • 4.7 发送 0 字节的数据是什么效果?
    • 4.8 connect 函数在阻塞和非阻塞模式下的行为
    • 4.9 连接时顺便接收第一组数据
    • 4.10 如何获取当前 socket 对应的接收缓冲区中有多少数据可读
    • 4.11 Linux EINTR 错误码
    • 4.12 Linux SIGPIPE 信号
    • 4.13 Linux poll 函数用法
    • 4.14 Linux epoll 模型
    • 4.15 高效的 readv 和 writev 函数
    • 4.16 主机字节序和网络字节序
    • 4.17 域名解析 API 介绍
  • 第5章网络通信故障排查常用命令

  • 第6章高性能网络通信协议设计精要

  • 第7章高性能服务结构设计

  • 第8章Redis 网络通信模块源码分析

  • 第9章后端服务重要模块设计探索

  • C++后端开发进阶
  • 第4章C++网络编程重难点解析
zhangxf
2023-04-05

4.7 发送 0 字节的数据是什么效果?

上一节,我提到 send 或 recv 函数返回 0 表示对端关闭了连接。有的读者可能认为如果发送 0 字节数据,那么 send 函数也会返回 0,对端会 recv 到 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)
 		{
 			//“假设recv返回值为0时是收到了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;
}
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81

上述代码侦听端口号是 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)
    	{
    		//发送了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;
}
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97

client 端连接服务器成功以后,每隔 3 秒调用 send 一次发送一个 0 字节的数据。除了先启动 server 以外,我们使用 tcpdump 抓一下经过端口 3000 上的数据包,使用如下命令:

tcpdump -i any 'tcp port 3000'
1

然后启动 client ,我们看下结果:

客户端确实是每隔 3 秒 send 一次数据。此时我们使用 lsof -i -Pn 命令查看连接状态,也是正常的:

然后,tcpdump 抓包结果输出中,除了连接时的三次握手数据包,再也无其他数据包,也就是说,send 函数发送 0 字节数据,此时 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
1
2
3
4
5
6

因此,server 端也会一直没有输出,如果你用的是 gdb 启动 server,此时中断下来会发现,server 端由于没有数据会一直阻塞在 recv 函数调用处(55 行)。

通过上面的测试我们得到如下结论:

存在以下两种情形会让 send 函数的返回值为 0 :

  • 对端关闭了连接时,我们正好尝试调用 send 函数发送数据;
  • 本端尝试调用 send 函数发送 0 字节数据。

而 recv 函数只有在对端关闭连接时才会返回 0,对端发送 0 字节,本端的 recv 函数是不会收到 0 字节数据的。

然而,这里的代码示例仅仅用于实验性讨论,send 一个 0 字节数据是没有任何意义的,希望读者在实际开发时,避免写出可能调用 send 函数发送 0 字节的代码。

上次更新: 2025/04/01, 20:53:14
4.6 socket 的阻塞模式和非阻塞模式
4.8 connect 函数在阻塞和非阻塞模式下的行为

← 4.6 socket 的阻塞模式和非阻塞模式 4.8 connect 函数在阻塞和非阻塞模式下的行为→

最近更新
01
第二章 关键字static及其不同用法
03-27
02
第一章 auto与类型推导
03-27
03
C++语言面试问题集锦 目录与说明
03-27
更多文章>
Copyright © 2024-2025 沪ICP备2023015129号 张小方 版权所有
  • 跟随系统
  • 浅色模式
  • 深色模式
  • 阅读模式