还原客户端send 0字节,服务端主动关闭后,客户端send 仍然返回0的全过程

还原客户端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,是否间接验证了以下结论并不严谨

你可能感兴趣的:(tcp协议)