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

抓包如下:

posted @ 2022-07-06 18:21  小夕nike  阅读(232)  评论(0编辑  收藏  举报