欢迎来到尧图网

客户服务 关于我们

您的位置:首页 > 教育 > 高考 > TCP shutdown 之后~

TCP shutdown 之后~

2024/10/24 10:25:09 来源:https://blog.csdn.net/qq_33724710/article/details/141296922  浏览:    关键词:TCP shutdown 之后~

目录

摘要

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 的接口:

NAMEshutdown - shut down part of a full-duplex connectionSYNOPSIS#include <sys/socket.h>int shutdown(int sockfd, int how);DESCRIPTIONThe  shutdown()  call causes all or part of a full-duplex connection on the socket associated with sockfdto be shut down.  If how is SHUT_RD, further receptions will be disallowed.  If how is  SHUT_WR,  furthertransmissions will be disallowed.  If how is SHUT_RDWR, further receptions and transmissions will be dis‐allowed.RETURN VALUEOn 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 forEPOLLHUP, 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_wait1sk->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 也会失败,真是无语!

版权声明:

本网仅为发布的内容提供存储空间,不对发表、转载的内容提供任何形式的保证。凡本网注明“来源:XXX网络”的作品,均转载自其它媒体,著作权归作者所有,商业转载请联系作者获得授权,非商业转载请注明出处。

我们尊重并感谢每一位作者,均已注明文章来源和作者。如因作品内容、版权或其它问题,请及时与我们联系,联系邮箱:809451989@qq.com,投稿邮箱:809451989@qq.com