首页 > 其他分享 >【转载】Socket 与 TCP 四次挥手(下)

【转载】Socket 与 TCP 四次挥手(下)

时间:2023-03-16 10:56:28浏览次数:45  
标签:addr Socket linger int TCP 四次 fd close socket

[转载](https://demonlee.tech/archives/2208002)

Socket 与 TCP 四次挥手(下)

Demon.Lee 2022年08月21日 206次浏览

本文实践环境:

Operating System: macOS 12.5
Kernel: Darwin Kernel Version 21.6.0
Architecture: ARM64

其他环境可能有些许差异,比如 socket 相关的宏定义等。

在前一篇的《Socket 与 TCP 四次挥手(上)》中,笔者对 TCP 关闭连接时的四次挥手流程进行了说明,并结合案例对 TIME_WAIT 状态进行了剖析。在这篇文章中,笔者将继续探讨 TCP 关闭连接时的相关问题,并重点讨论如何优雅地关闭连接。之所以 TCP 关闭连接时状态多,流程复杂,就是因为它是一个全双工协议,需要处理的细节多。

我们先来思考一个问题:为何 TCP 建立连接是三次握手,而关闭连接却是四次挥手?

挥手必须四次?

在《Socket 与 TCP 三次握手》这篇文章中,笔者其实已经提到了三次握手的来由:接收方将回复的 ACK 与建立服务器-客户端通道 SYN 合并成了一个数据包。但这其实并没有说明本质,那本质是什么呢?陶辉老师在网络课程《系统性能调优必知必会》中是这样说的:

因为 TCP 不允许连接处于半打开状态时就单向传输数据,所以在三次握手建立连接时,服务器会把 ACK 和 SYN 放在一起发给客户端,其中,ACK 用来打开客户端的发送通道,SYN 用来打开服务器的发送通道。这样,原本的四次握手就降为三次握手了。

那四次挥手能缩减到三次吗?

不行。连接处于半打开状态时不允许传输数据,但是处于半关闭状态时,TCP 连接是需要支持继续传输数据的。

这里需要稍微解释一下半关闭状态:

TCP 连接是双向的,每一方都有数据流的写入与读出。当主动方关闭一个方向的通道时,另一个方向还是可以正常传输数据的。比如互联网应用服务,当客户端发起请求后,随即关闭了往服务端的数据写入,此时服务端不能再从客户端(写入)- 服务端(读出)获取数据了;但是服务端还是可以将之前请求的响应数据通过服务端(写入)- 客户端(读出)通道发送回客户端。

这里服务端通过 Socket 把结果写给客户端时的状态就是半关闭的,服务端完成数据写入后,可以继续关闭剩下的半条连接。

正因为在半条连接上还需要进行数据传输,所以我们不能将四次挥手动作进行缩减。

有一个特殊情况,会将四次挥手变成三次挥手:主动方发送 FIN 报文,被动方回复 ACK 后立即调用 close() 函数,那么 ACK 与 FIN 两个包有可能合并成一个包发送给主动方。

上图中,双方都是调用 close() 函数关闭连接,但调用 close() 函数会将一方写入-读出通道同时关闭,也就无法达到前文所描述的半关闭状态的数据传输效果。怎么办?下文我们将会介绍 shutdown() 函数,用它替换 close() 就能搞定。在这之前,我们先来实战一把,用代码验证一下上篇文章中提到的 setsockopt() 函数是如何改变关闭连接时的行为的。

setsockopt

我们来看一下这个函数的定义:

➜  vifile > man 2 setsockopt
...
...
NAME
       getsockopt, setsockopt - get and set options on sockets

SYNOPSIS
#include <sys/types.h> /* See NOTES */
#include <sys/socket.h>

   int getsockopt(int sockfd, int level, int optname,
                  void *optval, socklen_t *optlen);
   int setsockopt(int sockfd, int level, int optname,
                  const void *optval, socklen_t optlen);

DESCRIPTION
getsockopt() and setsockopt() manipulate options for the socket referred to by the file descriptor sockfd. Options may exist at multiple protocol levels; they are always present at the uppermost socket level.

   When manipulating socket options, the level at <span class="hljs-built_in">which</span> the option resides and the name of the option must be specified.  To manipulate options at the sockets  API  level,  level is specified as SOL_SOCKET.  To manipulate options at any other level the protocol number of the appropriate protocol controlling the option is supplied.  For example, to indicate that an option is to be interpreted by the TCP protocol, level should be <span class="hljs-built_in">set</span> to the protocol number  of TCP; see getprotoent(3).

   The  arguments  optval  and optlen are used to access option values <span class="hljs-keyword">for</span> setsockopt().  For getsockopt() they identify a buffer <span class="hljs-keyword">in</span> <span class="hljs-built_in">which</span> the value <span class="hljs-keyword">for</span> the requested option(s) are to be returned.  For getsockopt(), optlen is a value-result argument, initially containing the size of the buffer pointed  to  by optval, and modified on <span class="hljs-built_in">return</span> to indicate the actual size of the value returned.  If no option value is to be supplied or returned, optval may be NULL.

   Optname  and  any specified options are passed uninterpreted to the appropriate protocol module <span class="hljs-keyword">for</span> interpretation.  The include file &lt;sys/socket.h&gt; contains definitions <span class="hljs-keyword">for</span> socket level options, described below.  Options at other protocol levels vary <span class="hljs-keyword">in</span> format and name; consult the  appropriate  entries <span class="hljs-keyword">in</span> section 4 of the manual.

   Most  socket-level  options  utilize an int argument <span class="hljs-keyword">for</span> optval.  For setsockopt(), the argument should be nonzero to <span class="hljs-built_in">enable</span> a boolean option, or zero <span class="hljs-keyword">if</span> the option is to be disabled.

   For a description of the available socket options see socket(7) and the appropriate protocol man pages.

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

   Netfilter allows the programmer to define custom socket options with associated handlers; <span class="hljs-keyword">for</span> such options, the <span class="hljs-built_in">return</span> value  on  success  is  the  value returned by the handler.

...
...

再看看 POSIX 的定义:

➜  vifile > man 3 setsockopt
...
...
NAME
       setsockopt — set the socket options

SYNOPSIS
#include <sys/socket.h>

   int setsockopt(int socket, int level, int option_name,
       const void *option_value, socklen_t option_len);

DESCRIPTION
The setsockopt() function shall set the option specified by the option_name argument, at the protocol level specified by the level argument, to the value pointed to by the option_value argument for the socket associated with the file descriptor specified by the socket argument.

   The  level  argument  specifies  the  protocol  level  at  <span class="hljs-built_in">which</span>  the  option  resides. To <span class="hljs-built_in">set</span> options at the socket level, specify the level argument as SOL_SOCKET. To <span class="hljs-built_in">set</span> options at other levels, supply the appropriate level identifier <span class="hljs-keyword">for</span> the protocol controlling the option.  For  example,  to  indicate that an option is interpreted by the TCP (Transport Control Protocol), <span class="hljs-built_in">set</span> level to IPPROTO_TCP as defined <span class="hljs-keyword">in</span> the &lt;netinet/in.h&gt; header.

   The  option_name  argument specifies a single option to <span class="hljs-built_in">set</span>. It can be one of the socket-level options defined <span class="hljs-keyword">in</span> &lt;sys_socket.h&gt; and described <span class="hljs-keyword">in</span> Section 2.10.16, Use of Options.  If option_name is equal to SO_RCVTIMEO or SO_SNDTIMEO and the implementation supports setting the  option,  it  is  unspecified whether  the  struct timeval pointed to by option_value is stored as provided by this <span class="hljs-keyword">function</span> or is rounded up to align with the resolution of the clock being used. If setsockopt() is called with option_name equal to SO_ACCEPTCONN, SO_ERROR, or SO_TYPE, the behavior is unspecified.

RETURN VALUE
Upon successful completion, setsockopt() shall return 0. Otherwise, −1 shall be returned and errno set to indicate the error.

ERRORS
The setsockopt() function shall fail if:
...
...

顺便提一句,这里的 man 2man 3 是啥意思?可以通过的 man man 了解:

➜  vifile > man man
...
...
The table below shows the section numbers of the manual followed by the types of pages they contain.
   1   Executable programs or shell commands
   2   System calls (<span class="hljs-built_in">functions</span> provided by the kernel)
   3   Library calls (<span class="hljs-built_in">functions</span> within program libraries)
   4   Special files (usually found <span class="hljs-keyword">in</span> /dev)
   5   File formats and conventions eg /etc/passwd
   6   Games
   7   Miscellaneous (including macro packages and conventions), e.g. man(7), groff(7)
   8   System administration commands (usually only <span class="hljs-keyword">for</span> root)
   9   Kernel routines [Non standard]

...
...
man -k printf
Search the short descriptions and manual page names for the keyword printf as regular expression. Print out any matches. Equivalent to apropos printf.
...
...

如果不知道某个命令或函数在哪个 page 中,可以通过 man -k xxx 查找(Linux 下),比如:

➜  vifile > man -k setsockopt
setsockopt (2)       - get and set options on sockets
setsockopt (3p)      - set the socket options
➜  vifile > 

好,我们再回来继续探讨 setsockopt() 函数:

int setsockopt(int socket, int level, int option_name,
               const void *option_value, socklen_t option_len);

从上面的描述中可以了解到 setsockopt() 函数的以下信息:

  • 可以对某个 Socket 进行设置,其中 socket 参数表示某个 Socket 的文件描述符,而 option_valueoption_len 则代表要修改的值及其长度,leveloption_name 则对应着在哪个级别的哪类选项上进行设置;

  • level :如果是对 Socket 级别的属性进行设置,那么这个值为:SOL_SOCKET,如果是其他级别,比如 TCP 级别,那么就需要设置 TCP 的协议号:IPPROTO_TCP

    // sys/socket.h
    

    /*

    • Level number for (get/set)sockopt() to apply to socket itself.
      */

    #define SOL_SOCKET 0xffff /* options for socket level */

    // ...

    // netinet/in.h
    

    /*

    • Protocols (RFC 1700)
      */

    #define IPPROTO_IP 0 /* dummy for IP */

    // ...

    #define IPPROTO_TCP 6 /* tcp */

    // ...

  • option_name :这里我们要改变关闭连接时的行为,所以对应的值为 SO_LINGER,如果想改变其他行为,则修改为对应的值,比如保持长连接的 SO_KEEPALIVE

    // sys/socket.h
    

    /*

    • Option flags per-socket.
      /
      #define SO_DEBUG 0x0001 /
      turn on debugging info recording /
      #define SO_ACCEPTCONN 0x0002 /
      socket has had listen() /
      #define SO_REUSEADDR 0x0004 /
      allow local address reuse /
      #define SO_KEEPALIVE 0x0008 /
      keep connections alive /
      #define SO_DONTROUTE 0x0010 /
      just use interface addresses /
      #define SO_BROADCAST 0x0020 /
      permit sending of broadcast msgs /
      #if !defined(_POSIX_C_SOURCE) || defined(_DARWIN_C_SOURCE)
      #define SO_USELOOPBACK 0x0040 /
      bypass hardware when possible /
      #define SO_LINGER 0x0080 /
      linger on close if data present (in ticks) /
      #else
      #define SO_LINGER 0x1080 /
      linger on close if data present (in seconds) /
      #endif /
      (!_POSIX_C_SOURCE || _DARWIN_C_SOURCE) /
      #define SO_OOBINLINE 0x0100 /
      leave received OOB data in line */

    // ...

  • SO_LINGER 对应的值类型为:linger,其定义为:

    // sys/socket.h
    

    /*

    • Structure used for manipulating linger option.
      /
      struct linger {
      int l_onoff; /
      option on/off /
      int l_linger; /
      linger time */
      };

    // ...

    l_onoff :0 表示关闭本选项,非 0 表示启用,默认值为 0;

    l_linger :等待时长,单位为秒,调用 Close() 函数后会阻塞,直到数据发送出去,或等待的时长到了,该值只有在 l_onoff 启用时才有效。

    1) l_onoff = 0,l_linger = 任意值:默认行为,如果 Socket 缓冲区中有数据,则会试着把数据发送出去;

    2) l_onoff = 非0,l_linger = 0:强行关闭,调用 Close() 函数后,会立刻发送 RST 标志给对方,该 TCP 连接跳过四次挥手,也就跳过了 TIME_WAIT 状态;如果 Socket 缓冲区中有数据也不管,直接丢弃;被动方也不知道对端已经彻底断开,只有当被动方阻塞在 recv() 调用上时,才会立即得到一个 Connection reset by peer 的异常。

    3) l_onoff = 非0,l_linger > 0:阻塞关闭,调用 Close() 函数后,对应的线程将会阻塞,直到缓冲区数据发送出去,或者计时时间到。

有了上面的描述,下面通过一个程序进行模拟验证:

// sockopt_server.c

#include <sys/socket.h>
#include <stdio.h>
#include <unistd.h>
#include <string.h>
#include <netinet/in.h>
#include <stdlib.h>
#include <errno.h>
#include "common/log.h"

#define READ_SIZE 1024

void bind_and_listen(char *const *argv, int listen_fd);

void read_data(int sock_fd, int flag) {
if (flag) {
printf("input> ");
getchar();
}

<span class="hljs-keyword">long</span> total = <span class="hljs-number">0</span>;
<span class="hljs-keyword">ssize_t</span> n;
<span class="hljs-keyword">char</span> buf[READ_SIZE];
<span class="hljs-keyword">for</span> (;;) {
    bzero(buf, <span class="hljs-keyword">sizeof</span>(buf));
    n = read(sock_fd, buf, READ_SIZE);
    <span class="hljs-keyword">if</span> (n == <span class="hljs-number">0</span>) {
        <span class="hljs-comment">// End-of-File,表示 socket 关闭</span>
        logging(<span class="hljs-string">"client socket closed"</span>);
        <span class="hljs-keyword">break</span>;
    } <span class="hljs-keyword">else</span> <span class="hljs-keyword">if</span> (n &lt; <span class="hljs-number">0</span>) {
        <span class="hljs-keyword">if</span> (EINTR == errno) {
            <span class="hljs-comment">// 非阻塞情况,继续 read</span>
            <span class="hljs-keyword">continue</span>;
        }
        error_logging(<span class="hljs-built_in">stderr</span>, <span class="hljs-string">"read from socket error"</span>);
        <span class="hljs-keyword">break</span>;
    }
    total += n;
}
<span class="hljs-keyword">char</span> msg[<span class="hljs-number">100</span>];
bzero(msg, <span class="hljs-keyword">sizeof</span>(msg));
<span class="hljs-built_in">sprintf</span>(msg, <span class="hljs-string">"read data size: [%ld]"</span>, total);
logging(msg);

}

int main(int argc, char **argv) {
if (argc < 2) {
error_handling(stderr, "Usage: sockopt_server <Port>");
}
int flag = 0;
if (3 == argc) {
flag = 1;
}

<span class="hljs-keyword">int</span> listen_fd = socket(AF_INET, SOCK_STREAM, <span class="hljs-number">0</span>);
bind_and_listen(argv, listen_fd);

<span class="hljs-class"><span class="hljs-keyword">struct</span> <span class="hljs-title">sockaddr_in</span> <span class="hljs-title">client_addr</span>;</span>
<span class="hljs-keyword">socklen_t</span> client_len;
<span class="hljs-keyword">int</span> client_fd;
<span class="hljs-keyword">for</span> (;;) {
    client_len = <span class="hljs-keyword">sizeof</span>(client_addr);
    client_fd = accept(listen_fd, (struct sockaddr *) &amp;client_addr, &amp;client_len);
    read_data(client_fd, flag);
    close(client_fd);
}

close(listen_fd);
<span class="hljs-keyword">return</span> <span class="hljs-number">0</span>;

}

void bind_and_listen(char *const *argv, int listen_fd) {
struct sockaddr_in server_addr;
bzero(&server_addr, sizeof(server_addr));
server_addr.sin_family = AF_INET;
server_addr.sin_port = htons(atoi(argv[1]));
server_addr.sin_addr.s_addr = htonl(INADDR_ANY);

<span class="hljs-keyword">if</span> (bind(listen_fd, (struct sockaddr *) &amp;server_addr, <span class="hljs-keyword">sizeof</span>(server_addr)) == <span class="hljs-number">-1</span>) {
    error_handling(<span class="hljs-built_in">stderr</span>, <span class="hljs-string">"bind error"</span>);
}
<span class="hljs-keyword">if</span> (listen(listen_fd, <span class="hljs-number">1024</span>) == <span class="hljs-number">-1</span>) {
    error_handling(<span class="hljs-built_in">stderr</span>, <span class="hljs-string">"listen error"</span>);
}

}

上面为服务端程序,接收多个连接,并一直接收数据,直到出错或客户端关闭。

下面为客户端程序,我们在建立连接之前调用了 setsockopt() 函数,另外在设置 SOL_SOCKET 的前后调用 getsockopt() 函数打印对应的值是否生效。这里为了简单起见,客户端发送完数据之后就关闭连接。

// sockopt_client.c

#include <stdlib.h>
#include <string.h>
#include <sys/socket.h>
#include <stdio.h>
#include <arpa/inet.h>
#include <unistd.h>
#include <errno.h>
#include "common/log.h"

# define MESSAGE_SIZE 20000000

void set_socket(int sock_fd, int linger_flag);

void display_socket_opt(int sock_fd);

void connect_server(char *const *argv, int sock_fd);

void send_msg(int sock_fd);

int main(int argc, char **argv) {
if (argc < 3) {
error_handling(stderr, "Usage: sockopt_client <IP> <Port> <linger_flag>");
}

<span class="hljs-keyword">int</span> sock_fd = socket(AF_INET, SOCK_STREAM, <span class="hljs-number">0</span>);
display_socket_opt(sock_fd);

<span class="hljs-keyword">int</span> linger_flag = (<span class="hljs-keyword">int</span>) strtol(argv[<span class="hljs-number">3</span>], (<span class="hljs-keyword">char</span> **) <span class="hljs-literal">NULL</span>, <span class="hljs-number">10</span>);
<span class="hljs-keyword">if</span> (errno != <span class="hljs-number">0</span>) {
    error_handling(<span class="hljs-built_in">stderr</span>, <span class="hljs-string">"convert linger_flag failed"</span>);
}
set_socket(sock_fd, linger_flag);
display_socket_opt(sock_fd);

connect_server(argv, sock_fd);
send_msg(sock_fd);

logging(<span class="hljs-string">"close socket now"</span>);
close(sock_fd);
logging(<span class="hljs-string">"close socket end..."</span>);

<span class="hljs-keyword">return</span> <span class="hljs-number">0</span>;

}

void send_msg(int sock_fd) {
char *msg = malloc(MESSAGE_SIZE + 1);
bzero(msg, MESSAGE_SIZE + 1);
for (long i = 0; i < MESSAGE_SIZE; ++i) {
msg[i] = '1';
}
logging("write data now");
ssize_t write_len = send(sock_fd, msg, MESSAGE_SIZE + 1, 0);
free(msg);

<span class="hljs-keyword">char</span> msg2[<span class="hljs-number">100</span>];
bzero(msg2, <span class="hljs-keyword">sizeof</span>(msg2));
<span class="hljs-built_in">sprintf</span>(msg2, <span class="hljs-string">"write data end: %lu"</span>, write_len);
logging(msg2);

}

void connect_server(char *const *argv, int sock_fd) {
struct sockaddr_in serv_addr;
bzero(&serv_addr, sizeof(serv_addr));
serv_addr.sin_family = AF_INET;
serv_addr.sin_port = htons(atoi(argv[2]));
//serv_addr.sin_addr.s_addr = inet_addr(argv[1]);
inet_pton(AF_INET, argv[1], &serv_addr.sin_addr);

<span class="hljs-keyword">int</span> connect_rt = connect(sock_fd, (struct sockaddr *) &amp;serv_addr, <span class="hljs-keyword">sizeof</span>(serv_addr));
<span class="hljs-keyword">if</span> (connect_rt &lt; <span class="hljs-number">0</span>) {
    error_handling(<span class="hljs-built_in">stderr</span>, <span class="hljs-string">"connect failed"</span>);
}

}

void set_socket(int sock_fd, int linger_flag) {
if (!linger_flag) {
return;
}
struct linger so_linger;
so_linger.l_onoff = 1;
if (linger_flag > 1) {
so_linger.l_linger = 1;
} else {
so_linger.l_linger = 0;
}
int ret = setsockopt(sock_fd, SOL_SOCKET, SO_LINGER, &so_linger, sizeof(so_linger));
if (ret < 0) {
error_handling(stderr, "setsockopt failed");
}
printf("setsockopt success...\n");
}

void display_socket_opt(int sock_fd) {
struct linger so_linger;
bzero(&so_linger, sizeof(so_linger));
socklen_t len = sizeof(so_linger);
int ret = getsockopt(sock_fd, SOL_SOCKET, SO_LINGER, &so_linger, &len);
if (ret < 0) {
error_handling(stderr, "getsockopt failed");
}
printf("getsockopt linger.l_onoff: %d, l_linger: %d\n", so_linger.l_onoff, so_linger.l_linger);
}

完整的代码可以在 Github 上获取[1]

编译之后运行,先启动服务端,客户端则根据参数的不同运行多次,并打开 Wireshark 进行抓包。

  • 场景一:默认行为
# sockopt_server 12305 1
➜  build git:(main) > ./bin/sockopt_server 12305 1          
input>  ## click `Enter` key
[2022-08-17 09:09:38]: client socket closed
[2022-08-17 09:09:38]: read data size: [20000001]

➜  build git:(main) > ./bin/sockopt_client 127.0.0.1 12305 0
getsockopt linger.l_onoff: 0, l_linger: 0
getsockopt linger.l_onoff: 0, l_linger: 0
[2022-08-17 09:09:18]: write data now
[2022-08-17 09:09:38]: write data end: 20000001
[2022-08-17 09:09:38]: close socket now
[2022-08-17 09:09:38]: close socket end...
➜  build git:(main) > 

程序运行正常,观察抓包情况,四次挥手也是正常的(注意这里 FIN 和数据是在一个包中发送的)。

服务端程序中传输的第 2 个参数,用来放开第 17-20 行,即程序一开始会阻塞在 getchar() 这一行,此时我们观察输入字符前后的 Socket 状态:

➜  build git:(main) >netstat -na | grep 12305    
tcp4       0      0  *.12305                *.*                    LISTEN     
➜  build git:(main) >netstat -na | grep 12305
tcp4  1045196     0  127.0.0.1.12305        127.0.0.1.63264        ESTABLISHED
tcp4       0 638508  127.0.0.1.63264        127.0.0.1.12305        ESTABLISHED
tcp4       0      0  *.12305                *.*                    LISTEN     
➜  build git:(main) >netstat -na | grep 12305
tcp4       0      0  *.12305                *.*                    LISTEN     
tcp4       0      0  127.0.0.1.63264        127.0.0.1.12305        TIME_WAIT  
➜  build git:(main) >netstat -na | grep 12305
tcp4       0      0  *.12305                *.*                    LISTEN     
➜  build git:(main) >

可以看到,客户端连接之前只有服务端一个处于 LISTEN 状态的 Socket,然后新增了两条 ESTABLISHED 状态的 Socket,当数据传输结束,客户端程序退出之后,服务端对应的 Socket 关闭。虽然客户端程序退出了,但客户端连接的 Socket 还处于 TIME_WAIT 状态(即在等待 2MSL),一段时间后,该 Socket 关闭。

  • 场景二:强制关闭
➜  build git:(main) >./bin/sockopt_server 12305 1          
input> 
[2022-08-17 09:32:42]: read from socket error: Connection reset by peer
[2022-08-17 09:32:42]: read data size: [16433097]

➜  build git:(main) >./bin/sockopt_client 127.0.0.1 12305 1
getsockopt linger.l_onoff: 0, l_linger: 0
setsockopt success...
getsockopt linger.l_onoff: 1, l_linger: 0
[2022-08-17 09:32:12]: write data now
[2022-08-17 09:32:42]: write data end: 20000001
[2022-08-17 09:32:42]: close socket now
[2022-08-17 09:32:42]: close socket end...
➜  build git:(main) >

可以看到,客户端设置 SO_LINGER 生效了,且发送数据正常;但服务端如前文描述的那样,会出现报错:“Connection reset by peer”,并且接收的数据丢失了一部分。观察抓包的结果,客户端发送了 RST 报文,强行关闭连接,跳过了四次挥手。

同样,我们来观察一下 Socket 状态:传输时,两个 Socket 的状态都是 ESTABLISHED,而数据传输结束后,两个 Socket 都关闭退出了,这也印证了强制关闭连接的结果。

➜  build git:(main) >netstat -na | grep 12305
tcp4       0      0  *.12305                *.*                    LISTEN     
➜  build git:(main) >netstat -na | grep 12305              
tcp4  1030580      0 127.0.0.1.12305        127.0.0.1.64208        ESTABLISHED
tcp4       0 540204  127.0.0.1.64208        127.0.0.1.12305        ESTABLISHED
tcp4       0      0  *.12305                *.*                    LISTEN     
➜  build git:(main) >netstat -na | grep 12305
tcp4       0      0  *.12305                *.*                    LISTEN     
➜  build git:(main) >
  • 场景三:阻塞关闭
➜  build git:(main) >./bin/sockopt_server 12305 
[2022-08-17 09:46:43]: client socket closed
[2022-08-17 09:46:43]: read data size: [20000001]

➜  build git:(main) >./bin/sockopt_client 127.0.0.1 12305 2
getsockopt linger.l_onoff: 0, l_linger: 0
setsockopt success...
getsockopt linger.l_onoff: 1, l_linger: 1
[2022-08-17 09:46:43]: write data now
[2022-08-17 09:46:43]: write data end: 20000001
[2022-08-17 09:46:43]: close socket now
[2022-08-17 09:46:43]: close socket end...
➜  build git:(main) >

第三种情况与第一种类似,客户端设置 SO_LINGER 生效了,数据传输正常(但并没有阻塞在 close() 函数上[2]),再通过四次挥手正常关闭了连接。

观察 Socket 状态,与第一种情况相同:

➜  build git:(main) >netstat -na | grep 12305    
tcp4       0      0  *.12305                *.*                    LISTEN     
➜  build git:(main) >netstat -na | grep 12305
tcp4  1045196     0  127.0.0.1.12305        127.0.0.1.64909        ESTABLISHED
tcp4       0 638508  127.0.0.1.64909        127.0.0.1.12305        ESTABLISHED
tcp4       0      0  *.12305                *.*                    LISTEN     
➜  build git:(main) >netstat -na | grep 12305
tcp4       0      0  *.12305                *.*                    LISTEN     
tcp4       0      0  127.0.0.1.64909        127.0.0.1.12305        TIME_WAIT  
➜  build git:(main) >netstat -na | grep 12305
tcp4       0      0  *.12305                *.*                    LISTEN     
➜  build git:(main) >

优雅地断开

close() vs shutdown()

前面聊了强制关闭,一般来说,这是要避免发生的。那如何做才能达到优雅关闭的效果呢?

在文章起始的部分,笔者其实已经给出了答案,调用 shutdown() 函数,而不是 close() 函数。观察下图,TCP 连接上有两个 I/O 流,一端的发送对应到另一端的接收,调用 close() 函数会将其中一方的写入和读取都关闭。关闭写入,对另一方没有太大影响,但关闭读取,那就有可能会导致数据丢失。

TCP 数据流

TCP 数据流

此处的 I/O 流,确实可以类比为水流,而且非常形象。

水朝着一个方向流动,同样,在 Socket 的流中,数据也只能向一个方向流动。因此,为了进行双向通信,需要上图所示的 2 个流。

好,现在我们就来看一下 shutdown() 函数的定义:

...
...
     #include <sys/socket.h>
 int shutdown(int socket, int how);

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

RETURN VALUES
The shutdown() function returns the value 0 if successful; otherwise the
value -1 is returned and the global variable errno is set to indicate the
error.

ERRORS
The call succeeds unless:

 [EBADF]            Socket is not a valid descriptor.
 [EINVAL]           The how argument is invalid.
 [ENOTCONN]         The specified socket is not connected.
 [ENOTSOCK]         Socket is a file, not a socket.

...
...

上面的描述已经非常清晰了,这里对第二个参数 int how 进行一些补充说明:

  • SHUT_RD(0),关闭输入流:对该 Socket 读数据将直接返回 EOF,即无法接收数据;若该 Socket 的接收缓冲区中还有数据,将会被抹去;若有新的数据流到达,会对数据进行 ACK,然后将数据悄悄丢弃,也就是说,对端还是会接收到 ACK,但并不知道数据已被丢弃。
  • SHUT_WR(1),关闭输出流,这就是常被称为 半关闭 的连接:无法对该 Socket 进行写操作,不管该 Socket 引用计数的值是多少,都会直接关闭连接的写方向;若该 Socket 的输出缓冲区中还有数据,将会被立即发送出去,并在最后发送一个 FIN 报文给对端。
  • SHUT_RDWR(2),同时中断输入输出流:这其实相当于分 2 次调用 shutdown() 函数,一次以 SHUT_RD 为参数,一次以 SHUT_WR 为参数。
// sys/socket.h

// ...

/*

  • howto arguments for shutdown(2), specified by Posix.1g.
    /
    #define SHUT_RD 0 /
    shut down the reading side /
    #define SHUT_WR 1 /
    shut down the writing side /
    #define SHUT_RDWR 2 /
    shut down both sides */

// ...

对比上一篇《Socket 与 TCP 四次挥手(上)》中介绍的 close() 函数,二者之间有什么区别呢?

  • 结合上面的 TCP 数据流示意图,shutdown() 根据入参可以控制只关闭一端数据流的一部分(如输入流或输出流),一般情况下会关闭输出流来实现优雅关闭连接;而 close() 是做不到的,这个在前文已经说过。
  • Socket 可以被多个进程共享,比如通过 fork 方式产生子进程,那么 Socket 引用计数将会递增,调用一次 close() ,引用计数将会递减,直到引用计数为 0 ,才会对 Socket 彻底释放,并关闭 TCP 两个方向的数据流。但 shutdown() 则不管引用计数,直接将 Socket 置为不可用,因此在多进(线)程共享 Socket 的情况下,可能会带来影响。
  • close() 会关闭连接,释放该连接对应的资源,但 shutdown() 只是关闭流,并不会释放 Socket 及其资源。
  • close() 并不一定会发出 FIN 报文,而 shutdown() 则总是会发出 FIN 报文(SHUT_RD 除外)。

综上所述, close()shutdown() 是有很大差异,并且 close() 也不等于 shutdown(SHUT_RDWR)

上面我们将调用 shutdown(SHUT_WR) 的连接称为半关闭连接,而一方调用 close() 的连接也有一个名字,叫作 孤儿 连接。

实战

这里我们使用《网络编程实战》课程中的代码示例进行验证。笔者对其进行了简单地调整,并已提交到 Github 上[3]

客户端 :shutdown_client.c

// shutdown_client.c

#include <stdlib.h>
#include <stdio.h>
#include <arpa/inet.h>
#include <strings.h>
#include <unistd.h>
#include "common/log.h"
#include "common/general.h"

int main(int argc, char **argv) {
if (argc < 3) {
error_handling(stderr, "Usage: shutdown_client <IP> <Port>");
}

<span class="hljs-class"><span class="hljs-keyword">struct</span> <span class="hljs-title">sockaddr_in</span> <span class="hljs-title">server_addr</span>;</span>
bzero(&amp;server_addr, <span class="hljs-keyword">sizeof</span>(server_addr));
server_addr.sin_family = AF_INET;
server_addr.sin_port = htons(atoi(argv[<span class="hljs-number">2</span>]));
inet_pton(AF_INET, argv[<span class="hljs-number">1</span>], &amp;server_addr.sin_addr);

<span class="hljs-keyword">int</span> socket_fd = socket(AF_INET, SOCK_STREAM, <span class="hljs-number">0</span>);
<span class="hljs-keyword">int</span> ret = connect(socket_fd, (struct sockaddr *) &amp;server_addr, <span class="hljs-keyword">sizeof</span>(server_addr));
<span class="hljs-keyword">if</span> (ret &lt; <span class="hljs-number">0</span>) {
    error_handling(<span class="hljs-built_in">stderr</span>, <span class="hljs-string">"client socket connect failed"</span>);
}

<span class="hljs-keyword">char</span> send_line[MAX_LINE], recv_line[MAX_LINE + <span class="hljs-number">1</span>];
<span class="hljs-keyword">ssize_t</span> n;

fd_set read_mask;
fd_set all_reads;

FD_ZERO(&amp;all_reads);
FD_SET(<span class="hljs-number">0</span>, &amp;all_reads);
FD_SET(socket_fd, &amp;all_reads);

<span class="hljs-keyword">for</span> (;;) {
    read_mask = all_reads;
    <span class="hljs-keyword">int</span> rc = select(socket_fd + <span class="hljs-number">1</span>, &amp;read_mask, <span class="hljs-literal">NULL</span>, <span class="hljs-literal">NULL</span>, <span class="hljs-literal">NULL</span>);
    <span class="hljs-keyword">if</span> (rc &lt; <span class="hljs-number">0</span>) {
        error_logging(<span class="hljs-built_in">stderr</span>, <span class="hljs-string">"select failed"</span>);
        <span class="hljs-keyword">break</span>;
    }

    <span class="hljs-keyword">if</span> (FD_ISSET(socket_fd, &amp;read_mask)) {
        n = read(socket_fd, recv_line, MAX_LINE);
        <span class="hljs-keyword">if</span> (n &lt; <span class="hljs-number">0</span>) {
            error_logging(<span class="hljs-built_in">stderr</span>, <span class="hljs-string">"read data error"</span>);
            <span class="hljs-keyword">break</span>;
        } <span class="hljs-keyword">else</span> <span class="hljs-keyword">if</span> (n == <span class="hljs-number">0</span>) {
            error_logging(<span class="hljs-built_in">stderr</span>, <span class="hljs-string">"server terminated"</span>);
            <span class="hljs-keyword">break</span>;
        }
        recv_line[n] = <span class="hljs-string">'\0'</span>;
        <span class="hljs-built_in">fputs</span>(recv_line, <span class="hljs-built_in">stdout</span>);
        <span class="hljs-built_in">fputs</span>(<span class="hljs-string">"\n"</span>, <span class="hljs-built_in">stdout</span>);
    }

    <span class="hljs-keyword">if</span> (FD_ISSET(<span class="hljs-number">0</span>, &amp;read_mask)) {
        <span class="hljs-keyword">if</span> (fgets(send_line, MAX_LINE, <span class="hljs-built_in">stdin</span>) == <span class="hljs-literal">NULL</span>) {
            <span class="hljs-keyword">continue</span>;
        }
        <span class="hljs-keyword">if</span> (<span class="hljs-built_in">strncmp</span>(send_line, <span class="hljs-string">"shutdown"</span>, <span class="hljs-number">8</span>) == <span class="hljs-number">0</span>) {
            FD_CLR(<span class="hljs-number">0</span>, &amp;all_reads);
            error_logging(<span class="hljs-built_in">stdout</span>, <span class="hljs-string">"shutdown socket_fd now"</span>);
            <span class="hljs-keyword">if</span> (shutdown(socket_fd, SHUT_WR)) {
                error_logging(<span class="hljs-built_in">stderr</span>, <span class="hljs-string">"shutdown failed"</span>);
                <span class="hljs-keyword">break</span>;
            }
        } <span class="hljs-keyword">else</span> <span class="hljs-keyword">if</span> (<span class="hljs-built_in">strncmp</span>(send_line, <span class="hljs-string">"close"</span>, <span class="hljs-number">5</span>) == <span class="hljs-number">0</span>) {
            FD_CLR(<span class="hljs-number">0</span>, &amp;all_reads);
            <span class="hljs-keyword">if</span> (close(socket_fd)) {
                error_handling(<span class="hljs-built_in">stderr</span>, <span class="hljs-string">"close failed"</span>);
            }
            sleep(<span class="hljs-number">6</span>);
            <span class="hljs-built_in">exit</span>(<span class="hljs-number">0</span>);
        } <span class="hljs-keyword">else</span> {
            <span class="hljs-keyword">size_t</span> i = <span class="hljs-built_in">strlen</span>(send_line);
            <span class="hljs-keyword">if</span> (send_line[i - <span class="hljs-number">1</span>] == <span class="hljs-string">'\n'</span>) {
                send_line[i - <span class="hljs-number">1</span>] = <span class="hljs-string">'\0'</span>;
            }
            <span class="hljs-built_in">printf</span>(<span class="hljs-string">"now sending: [%s]\n"</span>, send_line);
            <span class="hljs-keyword">ssize_t</span> rt = write(socket_fd, send_line, <span class="hljs-built_in">strlen</span>(send_line));
            <span class="hljs-keyword">if</span> (rt &lt; <span class="hljs-number">0</span>) {
                error_logging(<span class="hljs-built_in">stderr</span>, <span class="hljs-string">"write data error"</span>);
                <span class="hljs-keyword">break</span>;
            }
            <span class="hljs-built_in">printf</span>(<span class="hljs-string">"send bytes: %zu\n"</span>, rt);
        }
    }
}
<span class="hljs-comment">// sleep(5);</span>
error_logging(<span class="hljs-built_in">stdout</span>, <span class="hljs-string">"close socket_fd now"</span>);
close(socket_fd);
<span class="hljs-comment">// sleep(5);</span>

<span class="hljs-keyword">return</span> <span class="hljs-number">0</span>;

}

服务端 :shutdown_server.c

// shutdown_server.c

#include <stdlib.h>
#include <stdio.h>
#include <arpa/inet.h>
#include <strings.h>
#include <unistd.h>
#include "common/log.h"
#include "common/general.h"

static int count;

static void sig_int(int sig_no) {
printf("received signal: %d\n", sig_no);
printf("received %d datagrams.\n", count);
exit(0);
}

int main(int argc, char **argv) {
if (argc < 2) {
error_handling(stderr, "Usage: shutdown_server <Port>");
}

<span class="hljs-class"><span class="hljs-keyword">struct</span> <span class="hljs-title">sockaddr_in</span> <span class="hljs-title">server_addr</span>;</span>
bzero(&amp;server_addr, <span class="hljs-keyword">sizeof</span>(server_addr));
server_addr.sin_family = AF_INET;
server_addr.sin_port = htons(atoi(argv[<span class="hljs-number">1</span>]));
server_addr.sin_addr.s_addr = htonl(INADDR_ANY);

<span class="hljs-keyword">int</span> listen_fd = socket(AF_INET, SOCK_STREAM, <span class="hljs-number">0</span>);
<span class="hljs-keyword">int</span> ret = bind(listen_fd, (struct sockaddr *) &amp;server_addr, <span class="hljs-keyword">sizeof</span>(server_addr));
<span class="hljs-keyword">if</span> (ret &lt; <span class="hljs-number">0</span>) {
    error_handling(<span class="hljs-built_in">stderr</span>, <span class="hljs-string">"server socket bind failed"</span>);
}

ret = listen(listen_fd, LISTEN_QUEUE);
<span class="hljs-keyword">if</span> (ret &lt; <span class="hljs-number">0</span>) {
    error_handling(<span class="hljs-built_in">stderr</span>, <span class="hljs-string">"server socket listen failed"</span>);
}

signal(SIGINT, sig_int);
<span class="hljs-comment">// signal(SIGPIPE, sig_int);</span>

<span class="hljs-class"><span class="hljs-keyword">struct</span> <span class="hljs-title">sockaddr_in</span> <span class="hljs-title">client_addr</span>;</span>
<span class="hljs-keyword">socklen_t</span> client_len = <span class="hljs-keyword">sizeof</span>(client_addr);

<span class="hljs-keyword">int</span> conn_fd = accept(listen_fd, (struct sockaddr *) &amp;client_addr, &amp;client_len);
<span class="hljs-keyword">if</span> (conn_fd &lt; <span class="hljs-number">0</span>) {
    error_handling(<span class="hljs-built_in">stderr</span>, <span class="hljs-string">"server accept client socket failed"</span>);
}

<span class="hljs-keyword">char</span> message[MAX_LINE];
count = <span class="hljs-number">0</span>;

<span class="hljs-keyword">for</span> (;;) {
    <span class="hljs-keyword">ssize_t</span> n = read(conn_fd, message, MAX_LINE);
    <span class="hljs-keyword">if</span> (n &lt; <span class="hljs-number">0</span>) {
        error_logging(<span class="hljs-built_in">stderr</span>, <span class="hljs-string">"read message error"</span>);
        <span class="hljs-keyword">break</span>;
    } <span class="hljs-keyword">else</span> <span class="hljs-keyword">if</span> (n == <span class="hljs-number">0</span>) {
        error_logging(<span class="hljs-built_in">stderr</span>, <span class="hljs-string">"client closed"</span>);
        <span class="hljs-keyword">break</span>;
    }
    message[n] = <span class="hljs-string">'\0'</span>;
    <span class="hljs-built_in">printf</span>(<span class="hljs-string">"received %zu bytes: [%s]\n"</span>, n, message);
    count++;

    <span class="hljs-keyword">char</span> send_line[MAX_LINE];
    <span class="hljs-built_in">sprintf</span>(send_line, <span class="hljs-string">"message [%s] received."</span>, message);

    sleep(<span class="hljs-number">5</span>);

    <span class="hljs-built_in">printf</span>(<span class="hljs-string">"sending bytes: %s\n"</span>, send_line);
    <span class="hljs-keyword">ssize_t</span> write_nc = send(conn_fd, send_line, <span class="hljs-built_in">strlen</span>(send_line), <span class="hljs-number">0</span>);
    <span class="hljs-built_in">printf</span>(<span class="hljs-string">"send %zu bytes: %s\n"</span>, write_nc, send_line);
    <span class="hljs-keyword">if</span> (write_nc &lt; <span class="hljs-number">0</span>) {
        error_logging(<span class="hljs-built_in">stderr</span>, <span class="hljs-string">"send message error"</span>);
        <span class="hljs-keyword">break</span>;
    }
}
close(conn_fd);
close(listen_fd);

<span class="hljs-keyword">return</span> <span class="hljs-number">0</span>;

}

注意:

1、在客户端程序中使用了 select 多路复用,这里不展开,后续的学习笔记将会对其进行详细分析。

2、在服务端程序的第 42 行,我们先将 SIGPIPE 信号处理的逻辑给注释了,下文将对其进行解释。

3、在服务端回复消息之前,休眠 5 秒,用来模拟服务端程序的处理时长。

  • 场景一:客户端发送两条消息后,立即调用 close()
➜  build git:(main) ✗ >./bin/shutdown_server 12306          
received 3 bytes: [abc]
sending bytes: message [abc] received.
send 23 bytes: message [abc] received.
received 3 bytes: [cba]
sending bytes: message [cba] received.
➜  build git:(main) ✗ >
➜  build git:(main) ✗ >./bin/shutdown_client 127.0.0.1 12306
abc
now sending: [abc]
send bytes: 3
cba
now sending: [cba]
send bytes: 3
close
➜  build git:(main) ✗ >

从运行结果来看:

1、服务端收到了 2 条消息,并且发送了第一条的回复消息。

2、服务端第二条回复消息没有发送成功,程序便退出了。

3、客户端没有收到回复内容。

接着,我们查看一下抓包的情况:

在服务端发送第一条回复消息后,客户端回复了 RST 消息,表示连接已经断开。

基于前文对 close() 函数的分析,我们知道客户端收不到服务端后来发送的消息是合理的,但服务端程序莫名奇妙退出就有些令人困惑了。

其实正是这个 RST 消息,导致服务端程序异常退出了,为啥这么说呢?因为服务端发送第二条回复消息时,是向这个 RST 的 Socket 进行写操作,这会导致内核触发 SIGPIPE 信号。

如果未捕捉该信号,程序会如何执行?我们通过 man signal 查看一下该信号的默认处理:终止程序。

  • 场景二:捕捉 SIGPIPE 信号,客户端发送两条消息后,立即调用 close()

我们将服务端的第 42 行代码的注释去掉,并重新编译执行:

➜  build git:(main) ✗ >./bin/shutdown_server 12306                                  
received 3 bytes: [abc]
sending bytes: message [abc] received.
send 23 bytes: message [abc] received.
received 3 bytes: [cba]
sending bytes: message [cba] received.
received signal: 13
received 2 datagrams.
➜  build git:(main) ✗ >
➜  build git:(main) >./bin/shutdown_client 127.0.0.1 12306
abc
now sending: [abc]
send bytes: 3
cba
now sending: [cba]
send bytes: 3
close
➜  build git:(main) ✗ >

抓包数据如下:

从抓包数据上来看,与前一次并无区别,但服务端打印的日志却不同,通过捕捉信号,验证了我们的推理:

➜  build git:(main) ✗ >kill -l     
HUP INT QUIT ILL TRAP ABRT EMT FPE KILL BUS SEGV SYS PIPE ALRM TERM URG STOP TSTP CONT CHLD TTIN TTOU IO XCPU XFSZ VTALRM PROF WINCH INFO USR1 USR2
➜  build git:(main) ✗ >kill -l PIPE
13
➜  build git:(main) ✗ >
  • 场景三:捕捉 SIGPIPE 信号,客户端发送两条消息后,立即调用 shutdown(SHUT_WR)
➜  build git:(main) ✗ >./bin/shutdown_server 12306          
received 3 bytes: [abc]
sending bytes: message [abc] received.
send 23 bytes: message [abc] received.
received 3 bytes: [cba]
sending bytes: message [cba] received.
send 23 bytes: message [cba] received.
[2022-08-21 08:43:34]: client closed: Operation timed out
➜  build git:(main) ✗ >
➜  build git:(main) ✗ >./bin/shutdown_client 127.0.0.1 12306
abc
now sending: [abc]
send bytes: 3
cba
now sending: [cba]
send bytes: 3
shutdown
[2022-08-21 08:43:27]: shutdown socket_fd now: (null)
message [abc] received.
message [cba] received.
[2022-08-21 08:43:34]: server terminated: (null)
[2022-08-21 08:43:34]: close socket_fd now: (null)
➜  build git:(main) ✗ >

可以看到,客户端发送 shutdown 之后,收到了服务端的返回消息,并且服务端程序没有异常退出,而是通过正常流程结束进程。

我们再来看看网络抓包的结果:

如上图所示,客户端发送 FIN 报文后,服务端回复了对应的 ACK,然后服务端又发送了两条消息,最后才发送 FIN 报文,关闭连接。

我们可以放开客户端程序中最后两行休眠代码 // sleep(5); ,然后在新的终端运行 netstat -a -finet|grep 12306 命令查看 Socket 状态。如下所示,结果符合预期。

Proto Recv-Q Send-Q  Local Address          Foreign Address        (state)

tcp4 0 0 *.12306 . LISTEN
...
...
tcp4 0 0 localhost.12306 localhost.62159 ESTABLISHED
tcp4 0 0 localhost.62159 localhost.12306 ESTABLISHED
tcp4 0 0 *.12306 . LISTEN
...
...
tcp4 3 0 localhost.12306 localhost.62159 CLOSE_WAIT
tcp4 0 0 localhost.62159 localhost.12306 FIN_WAIT_2
tcp4 0 0 *.12306 . LISTEN
...
...
tcp4 0 0 localhost.62159 localhost.12306 TIME_WAIT
...
...

需要额外注意的是:

shutdown() 只是关闭流,并不会关闭 Socket ,并释放相关资源,所以还要继续调用 close() 函数来进行资源清理工作,只不过此时不会再发送 FIN 消息了。

查看客户端程序的最后,可以发现这行代码:close(socket_fd); ,即资源清理。

我们用一幅图对上面的场景进行总结(该图同样来源于网络课《网络编程实战》,笔者对其进行了重绘):

再谈关闭

如果让我们来实现 TCP 连接关闭,我们会怎么做?

其实这个问题的本质是:确定数据流什么时候结束。因为 TCP 连接上的数据流是没有边界的,双方都不知道对方什么时候关闭连接。所以,我们要做的,就是以什么样的方式来约定结束

1、用什么字符表示数据传输结束?

如果用一般的字符,比如 “bye”,肯定是不行的,因为传输的内容中可能就有相同的字符。所以,官方使用 EOF(end of file) 来表示内容传输结束。

那这个 EOF 又是个啥?

// stdio.h

#define EOF (-1)

EOF 为啥就可以?

阮一峰老师以前写过一篇文章《EOF是什么?》,推荐阅读(包括文章下面的评论),这里不做过多扩展。

接着我们查看 read() 函数的返回值说明,就明白了:

If successful, the number of bytes actually read is returned. Upon reading end-of-file, zero is returned. Otherwise, a -1 is returned and the global variable errno is set to indicate the error.

2、在什么时机发送结束标识符?

这个问题比较容易,当我们关闭输出流时,便发送 EOF,这相当于告诉对方准备关闭连接了。

好, TCP 四次挥手暂时总结到这里,关于数据流的读取和写入等函数(如 read()write()send() 等)的说明将在后续的学习笔记中输出,敬请期待。

参考资料


  1. https://github.com/LeoDemon/network-programming-in-action/tree/main/ch10 ↩︎

  2. 笔者调整了多次传输数据的大小,依然没有阻塞在 close() 函数上,可能测试的方式存在问题,待分析和优化。 ↩︎

  3. https://github.com/LeoDemon/network-programming-in-action/tree/main/ch11 ↩︎

</article>

标签:addr,Socket,linger,int,TCP,四次,fd,close,socket
From: https://www.cnblogs.com/LogicHan/p/17221486.html

相关文章

  • 自定义TCP协议,私有TCP协议以及TCP协议安全性提高升级
    tcp协议是最底层的,一般来说无需修改,直接使用。但是在某些场景下,直接使用会带来一些安全性的问题:比如物联网设备。物联网设备需要经常的发送消息到服务器,如果直接使用标准T......
  • TCP协议得物联网安全浅析
    公司做物联网项目,后端采用java+netty开发,端口如果直接暴露使之容易被扫描攻击。故实现自定义TCP头,这样可以在握手阶段就丢弃数据包,达到提高攻击门槛的目的。 在......
  • 记websocket调用feign失败
    在http中正常使用的feign接口,在websocket直接失败了,本来以为是没拿到对象,发现其实拿到了。后来发现feign接口如果已经被aop处理过,就会报jdkProxy的错误。修改一下AOP类的......
  • 三次握手和四次挥手
      ......
  • UDP协议类_DatagramSocket——广播代码实现
    广播地址:255.255.255.255 publicclassClientDemo{publicstaticvoidmain(String[]args)throwsIOException{//广播DatagramSocket客户端发送......
  • UDP协议类_DatagramSocket——组播代码实现
    组播地址:224.0.0.0--239.225.225.225,其中224.0.0.0--224.0.0.225为预留的组播地址,我们一般使用224.0.1.0及其之后的地址publicclassClientDemo{publicstaticv......
  • UDP协议类_DatagramSocket
    publicclassClientDemo{publicstaticvoidmain(String[]args)throwsIOException{//DatagramSocket客户端发送数据的步骤//1:创建Data......
  • contrack TCP协议状态转换
    索引和TCP状态值三维数组tcp_conntracks定义了TCP连接的状态转换表,第一维表示方向,0和1分别表示原和反方向;第二维表示6个报文标志,如下tcp_bit_set所示;第三位表示当前的......
  • 使用socket 和 httpURLConnection发起http请求
    publicstaticvoidtest()throwsException{//http://127.0.0.1:8080/logger/userInetAddressinetAddress=InetAddress.getByName("www.baidu.......
  • linux 网络管理之tcpdump命令详解
    一、tcpdump的作用tcpdump是linux环境的网络数据采集分析工具,也就是所谓的抓包工具,与tcpdump只有命令行格式不同,Windows有个图形可视化工具Wireshark所谓的抓包工具就......