tcp shutdown
环境:centos8 x86_64 内核:4.18.0
1. close() 与 shutdown()
我们知道,tcp 有 4 次挥手过程,对于主动端来说:
- 发送 fin 通知被动端连接即将关闭
- 等待被动端发送 fin 过来以彻底结束连接
如果进程通过调用 close() 来结束连接,会让 socket 直接关闭成为孤儿连接,即不再绑定任何进程。
不再绑定任何进程有如下影响:
- 接收缓存全部清空
- 在等待对端 fin 的过程中,如果对端还在持续发送数据,会回一个 rst 过去
- 主动端发送 rst 后,主动端 socket 就关闭了,不再经历 fin_wait2 和 time_wait
- 被动端收到 rst 后,被动端 socket 也直接关闭了,不再经历 close_wait 和 last_ack
- 被动端收到 rst 后,再次发送数据,会触发 sigpipe 信号(这里触发 sigpipe 信号明显是非正常四次挥手结束的连接才会触发)
调用 close() 的方式不太优雅:
- 接收缓存可能还有数据没有来得及读取
- 主动端等待 fin 的过程中,被动端可能还有数据需要发送,才能优雅的关闭 socket
基于此,shutdown() 函数出现了,其接口也非常简洁:
int shutdown(int socket, int how);
how 可以取值:
- SHUT_WR,即只关闭写方向
- SHUT_RD,即只关闭读方向
- SHUT_RDWR,即同时关闭读方向和写方向
Q:先调用 SHUT_RD 会发生什么?
A:我测试的内核版本什么都不会发生(tcp 状态不会改变,缓冲区不会清空,对端后续发送的数据也不会丢弃)
Q:调用 SHUT_RDWR 与 close() 的区别
A:没有区别
Q:先调用 SHUT_WR 会发生什么?
A:发送 fin 给对端,主动端接收缓冲区依然有效,也可以继续收取对端发送的数据(回复 ack 和通知应用层)
2. SHUT_WR 示例
客户端:
#include <string.h>
#include <unistd.h>
#include <stdio.h>
#include <stdlib.h>
#include <errno.h>
#include <assert.h>
#include <arpa/inet.h>
#include <sys/types.h>
#include <sys/socket.h>
#include <fcntl.h>
int set_nonblock(int fd) {
int old_flag = fcntl(fd, F_GETFL, 0);
int new_flag = old_flag | O_NONBLOCK;
if (fcntl(fd, F_SETFD, new_flag) < 0) {
fprintf(stderr, "fcntl failed: %s\n", strerror(errno));
return -1;
}
return 0;
}
int main (int argc, char* argv[]) {
if (argc != 2) {
printf("usage: ./mytest <port>\n");
return 0;
}
const char* host = "0.0.0.0";
int port = atoi(argv[1]);
struct sockaddr_in addr;
addr.sin_family = AF_INET;
addr.sin_addr.s_addr = inet_addr(host);
addr.sin_port = htons(port);
int fd = socket(AF_INET, SOCK_STREAM, 0);
int reuseaddr = 1;
setsockopt(fd, SOL_SOCKET, SO_REUSEADDR, &reuseaddr, sizeof(reuseaddr));
if (connect(fd, (struct sockaddr*)&addr, sizeof(addr)) != 0) {
printf("connect to %s:%d failed: %s\n", host, port, strerror(errno));
close(fd);
return 0;
}
printf("connect to %s:%d success\n", host, port);
assert(shutdown(fd, SHUT_WR) == 0);
int seconds = 2;
sleep(seconds);
assert(set_nonblock(fd) == 0);
int read_len = 100;
char read_buf[read_len];
int n = read(fd, read_buf, 100);
if (n > 0) {
read_buf[n] = '\0';
printf("read return: %s\n", read_buf);
}
return 0;
}
服务端:
#include <sys/types.h>
#include <sys/socket.h>
#include <arpa/inet.h>
#include <unistd.h>
#include <fcntl.h>
#include <stdio.h>
#include <stdlib.h>
#include <string.h>
#include <errno.h>
int main(int argc, char** argv) {
if (argc != 2) {
fprintf(stderr, "usage: ./server 3000\n");
return -1;
}
const char* host = "0.0.0.0";
int port = atoi(argv[1]);
int listenfd = socket(AF_INET, SOCK_STREAM, 0);
if (listenfd == -1) {
fprintf(stderr, "create listen fd failed\n");
return -1;
}
int on = 1;
setsockopt(listenfd, SOL_SOCKET, SO_REUSEADDR, (char*)&on, sizeof(on));
struct sockaddr_in bindaddr;
bindaddr.sin_family = AF_INET;
// bindaddr.sin_addr.s_addr = htonl(INADDR_ANY);
(void)inet_aton(host, &bindaddr.sin_addr);
bindaddr.sin_port = htons(port);
int ret = bind(listenfd, (struct sockaddr*)&bindaddr, sizeof(bindaddr));
if (ret == -1) {
fprintf(stderr, "bind listen fd failed: %s\n", strerror(errno));
close(listenfd);
return -1;
}
fprintf(stderr, "start listen on [%s:%d]\n", host, port);
// block call
ret = listen(listenfd, SOMAXCONN);
if (ret == -1) {
fprintf(stderr, "listen failed: %s\n", strerror(errno));
close(listenfd);
return -1;
}
struct sockaddr_in clientaddr;
socklen_t clientaddr_len = sizeof(clientaddr);
int clientfd = accept(listenfd, (struct sockaddr*)&clientaddr, &clientaddr_len);
if (clientfd == -1) {
fprintf(stderr, "accept failed: %s\n", strerror(errno));
close(listenfd);
return -1;
}
fprintf(stderr, "new client [%s, %d]\n",
inet_ntoa(clientaddr.sin_addr), htons(clientaddr.sin_port));
int seconds = 1;
sleep(seconds);
char cache[] = "hello, world\n";
if (write(clientfd, cache, strlen(cache)) == -1) {
printf("write to %s:%d failed: %s\n", host, port, strerror(errno));
}
seconds = 3;
sleep(seconds);
close(clientfd);
close(listenfd);
return 0;
}
日志如下:
服务端:
[user@centos8 shutdown]$ ./server 3000
start listen on [0.0.0.0:3000]
new client [127.0.0.1, 50510]
客户端:
[user@centos8 shutdown]$ ./client 3000
connect to 0.0.0.0:3000 success
read return: hello, world
抓包如下: