epoll 连接以后 recv始终返回0

@vrqq  June 1, 2018

起因

服务器端写了一个简单的epoll监听,代码如下(代码是错的啊!)

servfd = bind(0.0.0.0);
listen(servfd);

epollfd = create();
fcntl(server_sockfd, F_SETFL, fcntl(server_sockfd, F_GETFL, 0)|O_NONBLOCK);

struct epoll_event ev;
ev.events = (EPOLLIN|EPOLLET);
ev.data.fd = servfd;
epoll_ctl(epollfd, EPOLL_CTL_ADD, servfd, &servfd_ev);

while(1) {
  int size=epoll_wait();
  for(all in events[size]) {
    if (events[i] == servfd)
      conn = accept();
    else if (xxx)
      xxxxxx;
  }
}

然后我想试试它的稳定性啊,在客户机不断的run client -> ctrl+c -> run client -> ......
然后突然有一次,客户机连接上了,服务器端直接报 recv(clientfd, xxx) == 0
经查文档,返回0表示客户端告知服务器“关闭客户端向服务器的写通道”(发送FIN),使这条tcp通道处于“半连接状态”(server:),然后等待服务器向客户端发最后的一些数据并关闭“服务器向客户端的写通道”(关闭tcp连接)。
但是!客户端仍然在运行!tcp连接从服务器端和客户端看,都是ESTABLISHED状态。

然后目前服务器端状态如下:
连接前stat.jpg
(有很多CLOSE_WAIT)

多次启动client仍然是这样:服务器提示accept然后直接recv()==0然后服务器发出close(fd),但是这条链接仍然存在。

排查过程

然后试试nc呗 nc 192.168.186.142,然后服务器端居然提示recv()到一些消息,然后再次recv()==0。
nc这条tcp连接依然没有断掉,在nc的发出连接的瞬间,在服务器端netstat -antp发现有变化的一条:

tcp       49      0 192.168.186.142:12315   192.168.186.1:63358     CLOSE_WAIT  68104/./server

原来的状态是:close_wait但是没有所属PID。

好奇怪啊,似乎是新的连接继承了旧的连接,但是明明端口号不一样啊?
抓包看看,发现了client和server端的两个tcp通信:
一个是{server->client:63358 FIN},{client->server RST}
另一个是{client->server SYN}, {server->client SYN,ACK}, {client->server ACK} 正常的三步握手

思前想后。。。。。。
(可能机智的看客早已发现一切,上图第二条 Recv-Q == 11 有很多排队等待连接的,便说明了问题。)

原因如下了

先说说socket连接和file descriptor的关系。
linux的思想,一切皆文件。一条tcp连接,当被accept()以后,才有file descriptor(这个fd是属于这个thread所管辖的,其编号也在thread内部排序)。
客户端发起tcp连接,被服务器端kernel应答以后,连接状态直接变为ESTABLISHED,无论是否accept()。
无论是CLOSE_WAIT状态的连接,还是ESTABLISHED的连接。
说白了accept就是分配file descriptor。

我们的epoll是边缘触发,即:当状态有变化时候才触发。
因此其实是accept队列里面积攒了之前的已经被关掉的连接,导致accept直接受到了一个CLOSE_WAIT的socket连接。

或者把“accept用的socket”设为level trigger

可以换个角度理解问题
上述服务器代码运行着,然后同一时间 A、B、C三个客户端练进去了。
然后A、B、C到达了服务器kernel,三条链接在系统底层就被处理成了ESTABLISHED状态。
这三个连接进入accept()队列,但我们的程序只accept了一次,获得了A连接的fd。此时B、C两个仍然在accept队列里,但是我们的程序没理会。
然后我们把B、C两个ctrl+c,在服务器系统kernel看来:B、C两个连接变成了CLOSE_WAIT。注意:连接变换状态不会使epoll触发。

然后此时,A正常结束,双方close,这条链接从系统kernel去掉了。

然后我们尝试从客户机D发起连接,服务器端listening socket边缘触发,但是接到了“B的close_wait状态的连接”,才会出现上述结果。

很有趣,断网半天,神推理。
推荐一个小命令:lsof
晚安


添加新评论