TCP连接能发送一个0字节的数据包吗

"网络编程"

Posted by Simon on December 20, 2020

TCP连接能发送一个0字节的数据包吗

众所周知,一个TCP连接 sendrecv 函数返回 0 表示对端关闭了连接。但有人可能认为如果发送 0 字节数据,那么 send 函数也会返回 0,对端会 recv0 字节的数据,事实是这样的吗?

我们来通过一个例子,来看下 send 一个长度为 0 的数据,send 函数的返回值是什么,对端是否会 recv0 字节的数据。

server 端代码:

#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;
}

上述代码侦听端口号是 3000,代码 55 行调用了 recv 函数,如果客户端一直没有数据,程序会阻塞在这里。

client 端代码:

#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;
}

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

tcpdump -i any 'tcp port 3000'

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

image

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

image

然后,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 行)。

image

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

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

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

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