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的状态已经不存在了。

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 »