TypeCodes

Linux TCP通信出现CLOSE_WAIT后导致服务端进程挂掉

前文中讲述了Linux服务端TCP通信出现CLOSE_WAIT状态的原因,这篇文章主要通过一个实例演示它个一个“恶劣”影响:直接使服务端进程Down掉。

CentOS服务端建立监听端口

1 CentOS服务端建立监听端口

如上图所示,在虚拟机CentOS7服务器(192.168.1.178)中打开一个终端界面,建立8000端口的监听服务(PID:13035)。所用代码如下,和上一篇文章中的程序大体一样,只是多了一个SIGPIPE信号处理以及自动回复(Auto response from server.)部分。

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
/** 
 * @FileName    server_socket.c
 * @Describe    A simple example for creating a listen as a server and simulate generate a sigpipe signal in linux.
 * @Author      vfhky 2016-02-29 08:01 https://typecodes.com/cseries/tcpclosewaitsigpipe.html
 * @Compile     gcc server_socket.c -o server_socket
 */
#include <stdio.h>
#include <string.h>
#include <sys/types.h>
#include <sys/socket.h>
#include <netinet/in.h>
#include <arpa/inet.h>
#include <errno.h>
#include <signal.h>

//Whether add a signal handle.
#define SIGNAL_HANDLE 0

void sig_handle( int signal )
{
    printf( "Receive a signal=[%d].\n", signal );
    return;
}

int main( int argc, char **argv )
{
    int server_sockfd;
    int client_sockfd;
    int len;
    int llOpt = 1;
    struct sockaddr_in my_addr;
    struct sockaddr_in remote_addr;
    int sin_size;
    char buf[BUFSIZ];
    memset( &my_addr, 0, sizeof(my_addr) );
    my_addr.sin_family = AF_INET;
    my_addr.sin_addr.s_addr = INADDR_ANY;
    my_addr.sin_port = htons(8000);

    #if SIGNAL_HANDLE
    struct sigaction new_act, old_act;
    new_act.sa_handler = sig_handle;
    new_act.sa_flags = 0;
    sigemptyset( &new_act.sa_mask );
    sigaction( SIGPIPE, &new_act, &old_act );
    sigaction( SIGINT, &new_act, &old_act );    
    #endif

    if( ( server_sockfd = socket( AF_INET, SOCK_STREAM, 0 ) ) < 0 )
    {  
        perror("socket");
        return 1;
    }

    if( setsockopt( server_sockfd, SOL_SOCKET, SO_REUSEADDR, &llOpt, sizeof(llOpt) ) ) {
        close(server_sockfd);
        return errno;
    }

    if( bind( server_sockfd, (struct sockaddr *)&my_addr, sizeof(struct sockaddr) ) < 0 )
    {
        perror( "bind" );
        return 1;
    }

    listen( server_sockfd, 5 );

    sin_size = sizeof( struct sockaddr_in );

    printf( "Server is listening with pid=[%d].\n", getpid() );

    while(1)
    {
        if( ( client_sockfd = accept( server_sockfd, (struct sockaddr *)&remote_addr, &sin_size ) ) < 0 )
        {
            perror( "accept" );
            return 1;
        }
        //Print the ip address and port of client.
        printf( "Accept client[%s:%u].\n", inet_ntoa(remote_addr.sin_addr), ntohs(remote_addr.sin_port) );

        send( client_sockfd, "Auto response from server.", strlen("Auto response from server."), 0 );

        memset( buf, 0x00, BUFSIZ );
        while( ( len = recv( client_sockfd ,buf, BUFSIZ, 0) ) > 0 )
        {
            buf[len]='\0';
            printf( "Message from client=[%s]\n", buf );
        }
        close( client_sockfd );
    }
    close( server_sockfd );
    return 0;
}

2 在Linux中利用telnet命令创建一个客户端

新建一个shell脚本netstat_nap.sh,里面只包含一条有效命令netstat -nap|head -n 2;netstat -nap|grep 8000

再打开一个Linux终端界面,然后输入命令telnet 192.168.1.177 8000作为客户端建立与服务端的TCP连接。这时执行脚本./netstat_nap.sh可以看到Linux客户端(PID:13045)和服务端(PID:13035)的TCP通信已经变成ESTABLISHED状态,效果如下图所示:

Linux中利用netstat和lsof命令查看TCP服务状态

3 在Windows中利用telnet命令创建一个客户端

在Windows主机(192.168.1.110)中打开一个PowerShell终端界面,然后输入命令telnet 192.168.1.177 8000作为客户端建立与Linux服务端的TCP连接。

如下图所示,执行脚本./netstat_nap.sh,可以看到Windows客户端(端口:64012)和服务端(PID:13035)的TCP通信已经变成ESTABLISHED状态。同时使用命令lsof -i:8000,可以看到进程打开的文件。此时,处于LISTEN状态的13035进程对应的Recv-Q(表示内核中已经接收的队列)从0变成了1。

Linux中利用netstat和lsof命令查看TCP服务状态

4 直接关闭Windows telnet客户端界面并使用Wireshark抓包

在直接关闭telnet界面后,继续使用netstat_nap.sh脚本和lsof命令发现刚才建立的TCP通信出现了CLOSE_WAIT的状态。

Linux中利用netstat和lsof命令查看TCP服务状态

在等待2分钟后,在Windows中使用Wireshark抓包发现由于客户端发送了RST+ACK报文给Linux服务端,所以二者的TCP链路已经被复位了:

在Windows中使用Wireshark抓包

这时在Linux中再次使用netstat_nap.sh脚本和lsof命令发现CLOSE_WAIT的状态已经不存在了。但是从下图中可以看到处于LISTEN状态的Recv-Q对应的值还是为1,也就是由内核维护的已完成握手的队列中的这个连接还等待着服务端程序读取

Linux中利用netstat和lsof命令查看TCP服务状态

5 关闭Linux telnet客户端

在Windows关闭telnet客户端界面并发送RST+ACK报文后,关闭小节2中在Linux中打开的telnet客户端。这时Linux服务端进程会执行第90行处的close()函数,也即执行正常四次挥手关闭TCP连接。

接着Linux服务端进程继续从内核中已完成连接队列中取出已完成连接,这样之前小节3中Windows telnet建立的客户端连接被读取。如下图所示,服务端进程打印了第80行出的数据(Accept client[192.168.1.110:64012].),但是服务端进程却挂掉了。

CentOS服务端建立监听端口

这时在Linux中再次使用netstat_nap.sh脚本和lsof命令:

Linux中利用netstat和lsof命令查看TCP服务状态

6 原因分析

由于Windows客户端的TCP链路在小节4中由于RST的缘故而关闭了,没有读端。那么当Linux服务端执行82行的send()函数时,向之前的socket描述符发送26字节的报文数据时,会收到内核发送过来的SIGPIPE信号,导致服务端进程默认关闭。

因此,如果想捕捉到这个SIGPIPE信号的话,可以将程序17行的SIGNAL_HANDLE宏定义值改成1,那么就会得到如下图所示的情况(进程能正常运行了)。

Linux TCP通信出现CLOSE_WAIT后导致服务端进程挂掉

7 问题延伸

如果在第4小节中关闭Windows客户端界面后,又直接如第5小节所示关闭Linux telnet客户端界面,那么又会出现什么情况呢?于是又重新做了一遍测试,流程同上,下面是测试结果以及分析。

先用netstat和lsof命令查看TCP服务状态,发现监听服务正常:

Linux中利用netstat和lsof命令查看TCP服务状态

然后分别用TCPDUMP和Wireshark抓取TCP通信包,截取如下所示。可以发现在Linux telnet客户端完成四次挥手后,服务端进程继续向之前Windows telnet客户端建立的socket描述符发送26字节的报文数据。

因为Windows客户端此时处于FIN_WAIT2状态(Linux服务端处于CLOSE_WAIT状态),所以服务端能继续发其发送数据(即图中的PUSH+ACK报文),接着Windows客户端回应RST+ACK报文,从而两者的TCP链路复位。

在Linux中使用TCPDUMP抓包

在Windows中使用Wireshark抓包

这样Linux服务端进程还是能够正常执行监听任务:

服务端进程正常执行监听

8 其它

网上有人把这种客户端或者服务端异常关闭的连接叫做TCP半关闭(Half-Close),例如网线拔掉、突然断电等,此时对端连接仍认为双方连接处于打开中。

打赏支持

Comments »