目录
摘要
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 也会失败,真是无语!