还原客户端send 0字节,服务端主动关闭后,客户端send 仍然返回0的全过程
注:客户端和服务端都是非阻塞模式
### 四次挥手过程图示(客户端和服务器都可以主动发出断开连接)
参考:
TCP三次握手和四次挥手及wireshark抓取 - elephantcc - 博客园
[外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-S7SD1Hvk-1596600866827)(file:///Users/chen/Library/Application%20Support/marktext/images/2020-08-05-10-22-12-image.png)]
### 服务端代码:
/**
* 验证recv函数接受0字节的行为,server端,server_recv_zero_bytes.cpp
* zhangyl 2018.12.17
*/
#include
#include
#include
#include
#include
#include
#include
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;
}
### 客户端代码:
/**
* 验证非阻塞模式下send函数发送0字节的行为,client端,nonblocking_client_send_zero_bytes.cpp
* zhangyl 2018.12.17
*/
#include
#include
#include
#include
#include
#include
#include
#include
#include
#include
#define SERVER_ADDRESS "127.0.0.1"
#define SERVER_PORT 3000
#define SEND_DATA ""
int main(int argc, char* argv[])
{
signal(SIGPIPE,SIG_IGN);
//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.启动服务端程序:
[外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-0W4QRvXD-1596600866831)(file:///Users/chen/Library/Application%20Support/marktext/images/2020-08-05-09-59-05-image.png)]
### 2.启动客户端程序:
[外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-OVasIZXX-1596600866833)(file:///Users/chen/Library/Application%20Support/marktext/images/2020-08-05-09-59-53-image.png)]
### 3.此时启动tcpdump抓包并写入wirefile文件,用于观察服务端主动关闭后4次握手情况
[外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-8BIrQ8cL-1596600866837)(file:///Users/chen/Library/Application%20Support/marktext/images/2020-08-05-10-01-23-image.png)]
### 4.这时候ctrl+C关闭服务端程序,客户端保持运行,然后查看用wireshark打开tcpdump抓取的wirefile,如下
[外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-OHYwIfJ7-1596600866838)(file:///Users/chen/Library/Application%20Support/marktext/images/2020-08-05-10-06-24-image.png)]
### 5.由上图,可以看到服务端主动关闭发送FIN,然后客户端回复ACK,此时客户端进入CLOSE_WAIT状态,服务端进入FIN_WAIT2状态,用netstat可以看到这个状态
[外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-T21bvPXO-1596600866839)(file:///Users/chen/Library/Application%20Support/marktext/images/2020-08-05-10-10-22-image.png)]
### 6.此时查看客户端运行情况,可以看到客户端send函数依然返回0,但是客户端收到对端关闭没有收到调用close,所以客户端一直是CLOSE_WAIT状态
[外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-qEPd4tGM-1596600866840)(file:///Users/chen/Library/Application%20Support/marktext/images/2020-08-05-10-20-02-image.png)]
### 7.ctrl+C关闭客户端程序,程序退出会调用close,然后tcpdump抓取客户端关闭动作
[外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-Fhd8x7a4-1596600866845)(file:///Users/chen/Library/Application%20Support/marktext/images/2020-08-05-10-41-04-image.png)]
### 8.wireshark打开抓取的closefile可以看到客户端重新发起了四次挥手动作,由于服务早已关闭,所以收到了RST
[外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-0ainDu7K-1596600866847)(file:///Users/chen/Library/Application%20Support/marktext/images/2020-08-05-10-39-12-image.png)]
### 9.结论
这时候仍然没有结论,为什么会SEND会返回0,这时候需要结合SIGPIPE原理来分析,下面来还原下SIGPIPE信号产生的过程。
## SIGPIPE信号的产生过程
### SIGPIPE的产生原因参考:
Linux SIGPIPE信号产生原因与解决方法_自己的学习笔记-CSDN博客_sigpipe
### 代码
服务端代码不变
客户端代码send(clientfd, SEND_DATA, 0, 0); 改为send(clientfd, SEND_DATA, 1, 0);
因为要还原SIGPIPE过程,所以在服务端断开的时候,客户端需要发送数据,才能产生SIGPIPE
### 1.启动服务器
[外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-SykG94zX-1596600866848)(file:///Users/chen/Library/Application%20Support/marktext/images/2020-08-05-11-12-06-image.png)]
### 2.用gdb启动客户端
然后ctrl+c中断客户端,
[外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-V5pTdySE-1596600866849)(file:///Users/chen/Library/Application%20Support/marktext/images/2020-08-05-11-17-09-image.png)]
### 3.ctrl+C关闭服务端,然后单步跟踪客户端的运行情况
[外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-Igedn2kl-1596600866852)(file:///Users/chen/Library/Application%20Support/marktext/images/2020-08-05-11-18-00-image.png)]
### 4.客户端单步跟踪 可以看到客户端在第二次send的时候产生SIGPIPE
服务端关闭后,客户端第一次send ,可以看到ret=1,errno=0,可以看到成功发送
[外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-ykBQdfzS-1596600866853)(file:///Users/chen/Library/Application%20Support/marktext/images/2020-08-05-11-29-03-image.png)]
第二次send,可以看到ret返回-1,errno=32 ,32为SIGPIPE对应的错误码
[外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-Rm4g63iQ-1596600866854)(file:///Users/chen/Library/Application%20Support/marktext/images/2020-08-05-11-31-21-image.png)]
注: [外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-bNsVZpAj-1596600866854)(file:///Users/chen/Library/Application%20Support/marktext/images/2020-08-05-11-24-40-image.png)]
## 综合思考:
### 实验效果
SEND 1 代表客户端发送1字节代码(对应SEND 0字节实验)
SEND 0 代表客户端发送0字节代码(对应SIGPIPE实验)
为什么同样服务端关闭,send 0字节和send 1字节,客户端运行效果截然不同
–》SEND 1在服务端关闭后,第二次调用send函数会产生SIGPIPE
–》SEND 0在服务端关闭后,send 函数依然返回0
### 群主结论:
[外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-jYKJFW3Z-1596600866855)(file:///Users/chen/Library/Application%20Support/marktext/images/2020-08-05-11-56-52-image.png)]
### 综合结论:
是否可以换一种结论,SEND 0客户端在服务端关闭后,底层并没有真正发送出去(这点已用tcpdump验证),所以并不会产生SIGPIPE信号,而此时 SEND 0仍然只是SEND 0的0发送字节的返回值,并不是代表对端关闭,通常我们都是用recv 0代表对端关闭。
下面我们打印下SEND 0服务端关闭后send返回0时的错误码,
[外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-DXLT5dWL-1596600866856)(file:///Users/chen/Library/Application%20Support/marktext/images/2020-08-05-11-51-53-image.png)]可以看到errno = 0,说明了和服务端并没有交互,有交互必然会收到RST,是否间接验证了以下结论并不严谨