close fd 死循环_Linux TCP客户端出现CLOSE_WAIT后进入死循环

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

1a90de444c69704537c9ffbccdfd2627.png

1 CentOS服务端建立监听端口

如上图所示,在虚拟机CentOS7服务器(192.168.1.177)中打开一个终端界面,执行程序 linux_epoll_server_2 建立8000端口的监听服务(PID:2791)。进程的大体执行过程是使用epoll_wait等待客户端的接入,然后打印的接收报文并回复应答报文,最后关闭这个客户端的连接。

2 CentOS客户端连接服务端

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

aa72de2c96d05db13b6db5123dd9d0a6.png

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连接。

3e4595be40a118346d8a52145854b703.png

4 关键步骤:使用tcpdump命令抓取TCP通信包

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

bc106404f4879f2958d6ab27a54ee544.png

5 过程分析

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

1、tcpdump抓包分析:

[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字节的报文

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确认报文

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字节的报文

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确认报文

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状态)

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 的代码,所以监控不是很全,这里挑关键的3个部分进行分析:

[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_WAIT2状态,客户端进入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 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、客户端进程执行过程分析:

###### 客户端执行过程

[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 状态,而客户端立马回复了 ACK ,从而进入了 FIN_WAIT_2 状态。此时的客户端自然也进入了 CLOSE_WAIT 状态。

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

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

c77f6563d5843dccc3ee0173a60e5521.png

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

7 附录:

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

/**

* @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

#include

#include

#include

#include

#include

#include

#include

#include

#include

#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 buf[MAXLINE] = {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 OKrnContent-type: text/plainrnrn%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;

}

注意:本文来自TypeCodes。本站无法对本文内容的真实性、完整性、及时性、原创性提供任何保证,请您自行验证核实并承担相关的风险与后果!

CoLaBug.com遵循[CC BY-SA 4.0]分享并保持客观立场,本站不承担此类作品侵权行为的直接责任及连带责任。您有版权、意见、投诉等问题,请通过[eMail]联系我们处理,如需商业授权请联系原作者/原网站。

  • 0
    点赞
  • 0
    收藏
    觉得还不错? 一键收藏
  • 0
    评论

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值