前往小程序,Get更优阅读体验!
立即前往
首页
学习
活动
专区
工具
TVP
发布
社区首页 >专栏 >Linux TCP客户端出现CLOSE_WAIT后进入死循环

Linux TCP客户端出现CLOSE_WAIT后进入死循环

作者头像
typecodes
发布2024-03-29 14:48:58
1340
发布2024-03-29 14:48:58
举报
文章被收录于专栏:typecodestypecodes

前文中讲述了Linux服务端TCP的某个链路变成CLOSE_WAIT状态,然后由于客户端已经关闭了(发送了RST标志的报文),那么服务端如果继续向这个链路中写入数据的话就会收到SIGPIPE信号而终止,这篇文章主要通过客户端进入CLOSE_WAIT后由于收到服务端产生的RST标志报文进入死循环的情况。注:RST表示复位,用来关闭异常的连接。

CentOS服务端建立监听
CentOS服务端建立监听
1 CentOS服务端建立监听端口

如上图所示,在虚拟机CentOS7服务器(192.168.1.177)中打开一个终端界面,执行程序linux_epoll_server_2建立8000端口的监听服务(PID:2791)。进程的大体执行过程是通过epoll_wait等待客户端的接入,当可读描述符就绪时打印接收的报文并回复应答报文,最后调用close函数关闭这个描述符并将其从监听事件中删除。

2 CentOS客户端连接服务端

新建一个Linux会话终端并执行客户端程序linux_epoll_simple_sndmsg_netstat(具体代码见文末附录部分)。在过三次握手建立TCP连接后进程进入循环模式:每次发送完报文休眠5秒(sleep(5))接着再次向服务端发送报文。

客户端连接服务端
客户端连接服务端
3 使用netstat命令查看TCP状态

新建一个Linux会话终端并创建一个shell脚本linux_epoll_simple_sndmsg_netstat.sh ,里面包含关键命令sudo netstat -npt|head -n 2;sudo netstat -npa|grep 8006。这个脚本用于监控TCP的通信状态。从下图中可以看到,最终服务端进程(PID:2791)在监听8006端口,然后和客户端进程(PID:2804)建立了TCP连接。

使用netstat命令查看TCP状态
使用netstat命令查看TCP状态
4 关键步骤:使用tcpdump命令抓取TCP通信包

新建一个Linux会话终端并输入命令sudo tcpdump -i lo -n port 8006抓取客户端和服务端的TCP通信报文。

使用tcpdump命令抓取TCP通信包
使用tcpdump命令抓取TCP通信包
5 过程分析

根据前面步骤1服务端、步骤2客户端、步骤3netstat的监控以及步骤4中的TCPDUMP抓包做出具体的分析:

1、tcpdump抓包分析:

代码语言:javascript
复制
[vfhky@typecodes ~]$ sudo tcpdump -i lo -n port 8006
tcpdump: verbose output suppressed, use -v or -vv for full protocol decode
listening on lo, link-type EN10MB (Ethernet), capture size 65535 bytes

######三次握手
11:55:35.724646 IP 127.0.0.1.56710 > 127.0.0.1.8006: Flags [S], seq 1652959375, win 43690, options [mss 65495,sackOK,TS val 8433172 ecr 0,nop,wscale 7], length 0
13:09:58.084191 IP 127.0.0.1.8006 > 127.0.0.1.56710: Flags [S.], seq 2881349854, ack 1652959376, win 43690, options [mss 65495,sackOK,TS val 8433172 ecr 8433172,nop,wscale 7], length 0
11:55:35.724660 IP 127.0.0.1.56710 > 127.0.0.1.8006: Flags [.], ack 1, win 342, options [nop,nop,TS val 8433172 ecr 8433172], length 0

######客户端发送58字节的报文( seq 1:59中的59=seq(1)+length(58) )
11:55:35.724966 IP 127.0.0.1.56710 > 127.0.0.1.8006: Flags [P.], seq 1:59, ack 1, win 342, options [nop,nop,TS val 8433173 ecr 8433172], length 58
######服务端回复ACK确认报文(ACK=对方SEQ+报文长度=1+58=59)
11:55:35.724970 IP 127.0.0.1.8006 > 127.0.0.1.56710: Flags [.], ack 59, win 342, options [nop,nop,TS val 8433173 ecr 8433173], length 0

######服务端主动发送58字节的报文( seq 1:59中的59=seq(1)+length(58) )
11:55:35.725006 IP 127.0.0.1.8006 > 127.0.0.1.56710: Flags [P.], seq 1:59, ack 59, win 342, options [nop,nop,TS val 8433173 ecr 8433173], length 58
######客户端回复ACK确认报文(ACK=对方SEQ+报文长度=1+58=59)
11:55:35.725008 IP 127.0.0.1.56710 > 127.0.0.1.8006: Flags [.], ack 59, win 342, options [nop,nop,TS val 8433173 ecr 8433173], length 0

######服务端调用close函数关闭连接(发送FIN标志的报文后进入FIN_WAIT_1状态)
11:55:35.725018 IP 127.0.0.1.8006 > 127.0.0.1.56710: Flags [F.], seq 59, ack 59, win 342, options [nop,nop,TS val 8433173 ecr 8433173], length 0
######客户端回复ACK确认报文(客户端进入CLOSE_WAIT状态,服务端进入FIN_WAIT_2状态)
######ACK=对方SEQ+1=59+1=60
11:55:35.766501 IP 127.0.0.1.56710 > 127.0.0.1.8006: Flags [.], ack 60, win 342, options [nop,nop,TS val 8433215 ecr 8433173], length 0

######客户端5秒后再次发送58字节的报文(CLOSE_WAIT状态还是可以向对端发送报文的)
11:55:40.736161 IP 127.0.0.1.56710 > 127.0.0.1.8006: Flags [P.], seq 59:117, ack 60, win 342, options [nop,nop,TS val 8438184 ecr 8433173], length 58
######服务端发送RST链路重置标志的报文(客户端关闭)
11:55:40.736190 IP 127.0.0.1.8006 > 127.0.0.1.56710: Flags [R], seq 2881349914, win 0, length 0

2、netstat命令监控:由于脚本中做了sleep 1的操作,所以监控不是很及时,对小节4的图中关键的4个部分进行分析。其中Recv-Q对应的值为59,它不同于前文中LISTEN状态下Recv-Q对应的值(表示由内核完成的已就绪队列中的连接数),这里表示客户端接收缓存中有59字节的数据等待客户端进程去读取。另外为什么是59字节而不是服务端发送的58字节数据?LZ这里也不是很确定。

代码语言:javascript
复制
[vfhky@typecodes epoll]$ ./linux_epoll_simple_sndmsg_netstat.sh 
######服务端建立监听
Active Internet connections (w/o servers)
Proto Recv-Q Send-Q Local Address           Foreign Address         State       PID/Program name    
tcp        0      0 0.0.0.0:8006            0.0.0.0:*               LISTEN      2791/linux_epoll_se

######由于服务端接收并回复报文后主动调用close函数关闭了链路,服务端进入FIN_WAIT1状态,客户端进入CLOSE_WAIT状态。
Active Internet connections (w/o servers)
Proto Recv-Q Send-Q Local Address           Foreign Address         State       PID/Program name    
tcp        0      0 0.0.0.0:8006            0.0.0.0:*               LISTEN      2791/linux_epoll_se 
tcp       59      0 127.0.0.1:56710         127.0.0.1:8006          CLOSE_WAIT  2804/linux_epoll_si 
tcp        0      1 127.0.0.1:8006          127.0.0.1:56710         FIN_WAIT1   -

######客户端回复了ACK确认报文后,服务端进入FIN_WAIT2状态
Active Internet connections (w/o servers)
Proto Recv-Q Send-Q Local Address           Foreign Address         State       PID/Program name    
tcp        0      0 0.0.0.0:8006            0.0.0.0:*               LISTEN      2791/linux_epoll_se 
tcp       59      0 127.0.0.1:56710         127.0.0.1:8006          CLOSE_WAIT  2804/linux_epoll_si 
tcp        0      0 127.0.0.1:8006          127.0.0.1:56710         FIN_WAIT2   -

######由于服务端回复了RST标志的报文导致链路重置
Active Internet connections (w/o servers)
Proto Recv-Q Send-Q Local Address           Foreign Address         State       PID/Program name    
tcp        0      0 0.0.0.0:8006            0.0.0.0:*               LISTEN      2791/linux_epoll_se

3、客户端进程执行过程分析:

1 2 3 4 5

客户端执行过程 vfhky@typecodes epoll$ linux_epoll_simple_sndmsg i_send_len=58. //第1次发送报文(正常) i_send_len=58. //5秒后再次发送报文,那么会收到RST标志的应答报文 vfhky@typecodes epoll$
6 得出结论

通过小节5中的具体分析可以看到在服务端调用close函数关闭了客户端的连接后进入FIN_WAIT_1状态,那么客户端立马进入了CLOSE_WAIT状态。而服务端在收到客户端回复的ACK报文后进入FIN_WAIT_2状态。

因为处于CLOSE_WAIT状态的一方仍然可以向对端发送报文,所以客户端在休眠5秒后再次向服务端发送了58字节的报文。但是此时的服务端已经关闭了链路(FIN_WAIT_2状态),所以Linux内核自动发送了一个RST复位标志的报文给客户端。

但是为什么客户端进程在收到RST报文后会关闭呢?原因和《Linux TCP通信出现CLOSE_WAIT后导致服务端进程挂掉》是一样的,就是Linux内核产生软中断,发送SIGPIPE信号给客户端进程,导致其默认终止了。这点可以通过设置客户端程序中#define SIGNAL_HANDLE 0为1来验证,执行的效果如下图所示:

Linux TCP客户端出现CLOSE_WAIT后进入死循环
Linux TCP客户端出现CLOSE_WAIT后进入死循环

那么问题又来了,由于捕捉了SIGPIPE信号(对应值为13)后,客户端进程不会终止,所以进入了while死循环。同时由于捕捉了SIGINT信号(对应值为2),导致在客户端所在的Linux会话终端上无法使用Ctrl+C来终止进程,最后只能使用kill信号来终止客户端!

7 附录:

以上就是Linux TCP通信中客户端出现CLOSE_WAIT后进入死循环的一个实例以及分析过程,下面是客户端程序linux_epoll_simple_sndmsg_netstat.c,工作流程很简单。

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

/** * @FileName linux_epoll_simple_sndmsg.c * @Describe A simple example for creating a listen as a server and simulate generate a sigpipe signal in linux. * @Author vfhky 2017-03-10 12:49 https://typecodes.com/cseries/tcpclosewaitfinwaitrst1.html * @Compile gcc linux_epoll_simple_sndmsg.c -o linux_epoll_simple_sndmsg */ #include <stdio.h> #include <stdlib.h> #include <unistd.h> #include <errno.h> #include <string.h> #include <sys/types.h> #include <netinet/in.h> #include <sys/socket.h> #include <arpa/inet.h> #include <signal.h> #define IPADDR "127.0.0.1" #define SERVPORT 8006 #define MAXLINE 1024 #define SIGNAL_HANDLE 0 void sig_handle( int signal ) { printf( "Receive a signal=%d.\n", signal ); return; } int main( int argc, char **argv ) { #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 //发送缓存区 char bufMAXLINE = {0x00}; //成功发送的字节数 unsigned int i_send_len = 0; int sockfd; struct sockaddr_in serv_addr; //创建1个socket描述符 if((sockfd = socket(AF_INET,SOCK_STREAM,0)) == -1) { perror("socket error \n"); exit(1); } bzero( &serv_addr, sizeof(serv_addr) ); serv_addr.sin_family = AF_INET; serv_addr.sin_port = htons(SERVPORT); inet_pton( AF_INET, IPADDR, &serv_addr.sin_addr ); if( connect( sockfd, (struct sockaddr *)&serv_addr, sizeof(struct sockaddr) ) ==-1 ) { perror("connect \n"); exit(1); } printf( "sockfd=%d.\n", sockfd ); while( 1 ) { sprintf( buf, "HTTP/1.0 200 OK\r\nContent-type: text/plain\r\n\r\n%s", "Hello world!\n" ); if( ( i_send_len = write( sockfd, buf, strlen(buf) ) ) > 0 ) { printf( "i_send_len=%d.\n", i_send_len ); } sleep(5); } return 0; }

本文参与 腾讯云自媒体分享计划,分享自作者个人站点/博客。
原始发表:2017-03-10 ,如有侵权请联系 cloudcommunity@tencent.com 删除

本文分享自 作者个人站点/博客 前往查看

如有侵权,请联系 cloudcommunity@tencent.com 删除。

本文参与 腾讯云自媒体分享计划  ,欢迎热爱写作的你一起参与!

评论
登录后参与评论
0 条评论
热度
最新
推荐阅读
目录
  • 1 CentOS服务端建立监听端口
  • 2 CentOS客户端连接服务端
  • 3 使用netstat命令查看TCP状态
  • 4 关键步骤:使用tcpdump命令抓取TCP通信包
  • 5 过程分析
  • 6 得出结论
  • 7 附录:
领券
问题归档专栏文章快讯文章归档关键词归档开发者手册归档开发者手册 Section 归档