TCP shutdown 之后~

45 篇文章 2 订阅
8 篇文章 0 订阅

目录

摘要

1 API

2 shutdown(sockfd, SHUT_WR)

3 shutdown(sockfd, SHUT_WR)

4 kernel 是怎么做的?


摘要

        通过 shutdown() 关闭读写操作,会发生什么?具体点呢,考虑两个场景:

        场景一:C 发送数据完毕,想调用 shutdown 关闭写操作,这时候 TCP 数据包抓包是否可以看出 C 执行了这个操作;S 往 C 发数据后,C 是否还回 ACK?
        场景二:C 读取数据完毕,想调用 shutdown 关闭读操作,这时候 TCP 数据包抓包是否可以看出 C 执行了这个操作?S 继续往 C 发数据,整条 TCP 数据流会发生什么情况?

文中引用 Linux 内核源码基于版本 5.4.259,并做了一些删减以提高可读性。

1 API

        先来看下 shutdown 的接口:

NAME
       shutdown - shut down part of a full-duplex connection

SYNOPSIS
       #include <sys/socket.h>

       int shutdown(int sockfd, int how);

DESCRIPTION
       The  shutdown()  call causes all or part of a full-duplex connection on the socket associated with sockfd
       to be shut down.  If how is SHUT_RD, further receptions will be disallowed.  If how is  SHUT_WR,  further
       transmissions will be disallowed.  If how is SHUT_RDWR, further receptions and transmissions will be dis‐
       allowed.

RETURN VALUE
       On success, zero is returned.  On error, -1 is returned, and errno is set appropriately.

        shutdown 接口比较简单,仅需只需要传入文件描述符与执行的动作这两个参数即可。

2 shutdown(sockfd, SHUT_WR)

        先来看场景一:C 发送数据完毕,想调用 shutdown 关闭写操作,这时候 TCP 数据包抓包是否可以看出 C 执行了这个操作;S 往 C 发数据后,C 是否还回 ACK?

        能否看出 C 执行了这个操作呢?那我们需要知道 close 的表现:如果是 close 一个 socket,相当于直接关闭了读写,发 FIN,后续收到包会回 RST。

        对于 shutdown 关闭写的场景,,只是关闭了写,那还是可以读的,所以 C 仍然会继续回 ACK,能否看出 C  执行了这个操作呢?关闭写应当会发送一个 FIN,而后续收到数据又会继续回 ACK, 所以应该是能区分出来才对?最好的方式就是写个代码验证了:

        搞一个 server:

void server_process(int sock)
{
    char buf[10240];

    while (1) {
        ssize_t ret = recv(sock, buf, sizeof(buf), 0);
        if (ret < 0) {
            if (errno != EAGAIN) {
                printf("server recv failed: %s\n", strerror(errno));
                break;
            }
            continue;
        } 

        if (ret == 0) {
            printf("read end!\n");
            break;
        }

        buf[ret] = 0;
        size_t ret_s = send(sock, buf, ret, 0);
        printf("resp:%s %d/%d\n", buf, ret_s, ret);
    }
}

int do_server()
{
    int sock = socket(AF_INET, SOCK_STREAM, IPPROTO_TCP);
    if (sock < 0) {
        printf("server socket failed: %s\n", strerror(errno));
        return -1;
    }

    uint32_t ip;
    inet_aton(g_server_ip, (struct in_addr *)&ip);

    struct sockaddr_in addr;
    memset(&addr, 0, sizeof(addr));
    addr.sin_family = AF_INET;
    addr.sin_addr.s_addr = ip;
    addr.sin_port = htons(g_server_port);

    if (bind(sock, (struct sockaddr *)&addr, (socklen_t)sizeof(addr)) < 0) {
        printf("server bind failed: %s\n", strerror(errno));
        return -1;
    }

    if (listen(sock, 10) < 0) {
        printf("listen failed: %s\n", strerror(errno));
        return -1;
    }

    while (1) {
        int new_sock = accept(sock, NULL, NULL);
        if (new_sock < 0) {
            continue;
        }

        server_process(new_sock);
        close(new_sock);
    }

    return 0;
}

        有 server 必有 client:

int do_client()
{
    int sock = socket(AF_INET, SOCK_STREAM, IPPROTO_TCP);
    if (sock < 0) {
        printf("client socket failed: %s\n", strerror(errno));
        return -1;
    }

    uint32_t ip;
    inet_aton(g_server_ip, (struct in_addr *)&ip);

    struct sockaddr_in addr;
    memset(&addr, 0, sizeof(addr));
    addr.sin_family = AF_INET;
    addr.sin_addr.s_addr = ip;
    addr.sin_port = htons(g_server_port);

    if (connect(sock, (struct sockaddr *)&addr, (socklen_t)sizeof(addr)) < 0) {
        printf("client connect failed: %s\n", strerror(errno));
        return -1;
    }

    char buf[10240];
    int i = 0;
    while (1) {
        int n = snprintf(buf, sizeof(buf), "echo %d", i++);
        size_t ret_s = send(sock, buf, n, 0);
        if (ret_s != n) {
            break;
        }

        //if (shutdown(sock, SHUT_RD) < 0) {
        //    printf("shutdown failed: %s\n", strerror(errno));
        //} 

        ssize_t ret = recv(sock, buf, sizeof(buf), 0);
        if (ret == 0) {
            printf("read end!\n");
            break;
        }
        if (ret < 0) {
            if (errno != EAGAIN) {
                printf("client recv failed: %s\n", strerror(errno));
                break;
            }
            continue;
        }

        buf[ret] = 0;
        printf("resp:%s %d/%d\n", buf, ret, n);
        sleep(1);
    }

    return 0;
}

        我们用 client 模拟角色 C,server 模拟角色 S,通过在 client 中添加 shutdown 调用复现场景。修改代码前,默认输出如下:

         我们在 client 发送后,shutdown 关闭写,并通过 sleep 阻塞住循环,观察输出与抓包结果:

    

        可以看出,client shutdown 关闭写发了一个 FIN,随后server 回了 length 6 的数据,并且 client 仍然继续响应了 ACK。所以是可以跟 close 关闭 socket 区分开的。

3 shutdown(sockfd, SHUT_WR)

        同样的,修改代码,client 发送之后关闭读,修改后 client 代码如下:

int do_client()
{
    int sock = socket(AF_INET, SOCK_STREAM, IPPROTO_TCP);
    if (sock < 0) {
        printf("client socket failed: %s\n", strerror(errno));
        return -1;
    }

    uint32_t ip;
    inet_aton(g_server_ip, (struct in_addr *)&ip);

    struct sockaddr_in addr;
    memset(&addr, 0, sizeof(addr));
    addr.sin_family = AF_INET;
    addr.sin_addr.s_addr = ip;
    addr.sin_port = htons(g_server_port);

    if (connect(sock, (struct sockaddr *)&addr, (socklen_t)sizeof(addr)) < 0) {
        printf("client connect failed: %s\n", strerror(errno));
        return -1;
    }

    char buf[10240];
    int i = 0;
    while (1) {
        int n = snprintf(buf, sizeof(buf), "echo %d", i++);
        size_t ret_s = send(sock, buf, n, 0);
        if (ret_s != n) {
            break;
        }

        if (shutdown(sock, SHUT_RD) < 0) {
            printf("shutdown failed: %s\n", strerror(errno));
        } 

        /*ssize_t ret = recv(sock, buf, sizeof(buf), 0);
        if (ret == 0) {
            printf("read end!\n");
            break;
        }
        if (ret < 0) {
            if (errno != EAGAIN) {
                printf("client recv failed: %s\n", strerror(errno));
                break;
            }
            continue;
        }

        buf[ret] = 0;
        printf("resp:%s %d/%d\n", buf, ret, n);*/
        sleep(100);
    }

    return 0;
}

        直接看输出:

         数据流看不出变化,跟未执行 shutdown 关闭读操作的 TCP 流表现是一样的。

4 kernel 是怎么做的?

        直接看下 shutdown 的源码就知道了,用户层调用 shutdown,首先通过系统调用进来,随后调用到 inet 层的 inet_shutdown 函数:

// net/ipv4/af_inet.c
int inet_shutdown(struct socket *sock, int how)
{
	struct sock *sk = sock->sk;
	int err = 0;

	// 一些状态检查
	switch (sk->sk_state) {
	case TCP_CLOSE:
		err = -ENOTCONN;
		/* Hack to wake up other listeners, who can poll for
		   EPOLLHUP, even on eg. unconnected UDP sockets -- RR */
		/* fall through */
	default:
		sk->sk_shutdown |= how;
		if (sk->sk_prot->shutdown)
			sk->sk_prot->shutdown(sk, how);
		break;

	/* Remaining two branches are temporary solution for missing
	 * close() in multithreaded environment. It is _not_ a good idea,
	 * but we have no choice until close() is repaired at VFS level.
	 */
	case TCP_LISTEN:
		if (!(how & RCV_SHUTDOWN))
			break;
		/* fall through */
	case TCP_SYN_SENT:
		err = sk->sk_prot->disconnect(sk, O_NONBLOCK);
		sock->state = err ? SS_DISCONNECTING : SS_UNCONNECTED;
		break;
	}

	// 设置tcp连接状态, 比如从 estab -> fin_wait1
	sk->sk_state_change(sk);
	release_sock(sk);
	return err;
}

        我们看已连接场景下的流程,也就是 switch 中的 default分支,这里将 how 动作保存在了 shutdown 标记中,然后继续调用到 tcp 协议自己的 shutdown:

// net/ipv4/tcp.c
void tcp_shutdown(struct sock *sk, int how)
{
	if (!(how & SEND_SHUTDOWN))
		return;

	/* If we've already sent a FIN, or it's a closed state, skip this. */
	if ((1 << sk->sk_state) &
	    (TCPF_ESTABLISHED | TCPF_SYN_SENT |
	     TCPF_SYN_RECV | TCPF_CLOSE_WAIT)) {
		/* Clear out any half completed packets.  FIN if needed. */
		if (tcp_close_state(sk))
			tcp_send_fin(sk);
	}
}

tcp_shutdown 中,首先判断不是关闭写的话,就直接 return 了,所以 shutdown 关闭读,真的就只是记录了一个标记,连 socket 状态也没有发生改变。如果是关闭写,则会走到 tcp_close_state、tcp_send_fin,其实就是将 state 转移到下一个状态,即 FIN_WAIT1:

static const unsigned char new_state[16] = {
  /* current state:        new state:      action:	*/
  [0 /* (Invalid) */]	= TCP_CLOSE,
  [TCP_ESTABLISHED]	= TCP_FIN_WAIT1 | TCP_ACTION_FIN,
  ...
};

static int tcp_close_state(struct sock *sk)
{
	int next = (int)new_state[sk->sk_state];
	int ns = next & TCP_STATE_MASK;

	tcp_set_state(sk, ns);

	return next & TCP_ACTION_FIN;
}

        看到这里,我们也能将原理同测试的现象对应起来了,也就那样~

最后附上完整的测试代码,有 linux 和 windows 的:

https://github.com/Fireplusplus/Linux/tree/master/tcp_shutdown

另外,windows 下关闭读的表现不太一样,C 继续收到数据会回 RST, 并且 C 继续 send 也会失败,真是无语!

  • 18
    点赞
  • 8
    收藏
    觉得还不错? 一键收藏
  • 打赏
    打赏
  • 0
    评论
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包

打赏作者

Fireplusplus

你的鼓励将是我创作的最大动力

¥1 ¥2 ¥4 ¥6 ¥10 ¥20
扫码支付:¥1
获取中
扫码支付

您的余额不足,请更换扫码支付或充值

打赏作者

实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值