文章参考:epoll触发事件的分析EPOLLHUP&EPOLLRDHUPman_page

相信很多时候,大家都是通过检测 read/recv 返回 0 来判断对端是否关闭了连接,如果返回 0,我们通常也会 close 该连接。这没有问题,但在很多场景下有个缺陷:FIN 报文和普通数据报文一样,也是需要在缓冲区中排队的,只有当 read 读取到 FIN 以后才会返回 0,而且 FIN 报文无法和数据同时被读取,也就是说,必须将数据 read 完毕后,再调用一次 read 才能读取到 FIN 并返回 0 。这也是为什么在网络读取时需要将 read 放在循环中的原因之一,不仅是为了将数据读取完整,也是为了能够读到 FIN 报文。

注意,FIN 报文虽然会排队,但当本端收到 FIN 后,内核网络栈会立刻回复 ACK,而不会管你是否 read 到这个 FIN 报文。

那么这个缺陷会引发什么问题呢?由于笔者现在也是网络编程初学者,没有太多实战经验,所以这里只提供本人猜想的两个情景:

  • 在一个高并发网络场景下,服务器收到了对端发来的 FIN 报文(对端 close),但没有立即读取(正忙于处理已接收的数据),所以现在此连接处于半关状态,因为服务器的 read 还没有返回 0。直到服务器处理完其他事情后 read 并返回 0 才会 close 此连接。问题在于,这段时间内该连接被白白占用了,浪费了服务器的端口,这对高并发处理是不利的。服务器完全可以先关闭该连接,再去处理数据。
  • 客户端向服务器发送文件,而文件的末端是 EOF,所以当服务器 read 到文件末端的 EOF 后返回 0,进而关闭连接。问题来了,万一客户还想继续发送文件呢?也就是说,此时 read 返回 0 并不代表客户端想关闭连接

因此,我们应该尽量避免使用 read/recv 返回 0 来判断对端的关闭状态。那还有什么方法?答案是 epoll 的 EPOLLRDHUP 和 EPOLLHUP 事件。这两者很容易混淆,下面略作区分。

EPOLLRDHUP 最为常用,当对方关闭(close)连接或者关闭写(shutdown(SHUT_WR))时,本事件就会被触发 。所以 EPOLLRDHUP 被用来监听对方的连接状态。与前面 read 不同,只要 FIN 报文进入了缓冲区,不管是否读取,都会引发 EPOLLRDHUP 事件

那么当 EPOLLRDHUP 发生时,我们该做什么呢?因为我不知道对方是 close 还是 shutdown(SHUT_WR),如果是后者,我就还能够将处理好的数据发给对方,如果是前者,则发送数据后则会收到对方发来的 RST 报文,从而直接结束连接。 该做什么应该取决于应用场景,如果是 http 服务,那就不应该直接关闭连接,因为对端可能是 shutdown,且还需要接收数据(比如请求图片);如果是上传文件到服务器,那么就可以直接关闭连接,因为服务器不需要再向对端回复数据。

EPOLLHUP 则令人困惑,官方文档的解释是:当套接字挂起时,本事件被激发。然而什么是“挂起”却没有解释,网络讨论也说法不一。笔者给出两种已经被实验证实的情况:

  • 收到对端发来的 RST 报文

    RST 报文用来重置连接,当一方发送RST报文后,对端会立即关闭连接,并释放相关资源。所以收到 RST 后,套接字相当于残废,被“挂起”。

  • 将一个不可能触发该事件发生的套接字加入 EPOLL

    比如,使用 socket() 返回了一个套接字,既不 listen 也不 connect,这个套接字将没有任何事情发生(这也许就是“挂起”的含义),此时如果将其加入到 EPOLL 中,则会产生 EPOLLHUP 事件。

    值得一提的是,对端 close 连接时,不会触发本端的 EPOLLHUP;但对端同时 shutdown 读和写时(即 shutdown(SHUT_RDWR) ),则会触发本端的 EPOLLHUP因为调用 shutdown(SHUT_RDWR) 只会关闭连接的读端和写端,不会释放文件描述符和其他相关资源,但此时该套接字已经处于“聋哑”状态,没有作用了,所以相当与被“挂起”;而当关闭(close)套接字时,内核会自动将套接字描述符从 epoll 中删除,因此本端不会再触发任何事件 ,如果应用程序需要释放文件描述符和相关资源,还需要调用 close 函数。

注意,EPOLLHUP 不能用来监听对方的关闭状态!

补充说明:

  • 对于 EPOLLERR 和 EPOLLHUP,不需要在 epoll_event 时针对 fd 作设置,一样也会触发
  • 对端发来 RST 信号,触发本端的 EPOLLIN + EPOLLRDHUP + EPOLLHUP + EPOLLERR 事件
  • 如前文,对端不管是 close 套接字,还是 shutdown 写,本端触发的都是 EPOLLIN + EPOLLRDHUP 事件,因此,本端无从区分对端是 close 了套接字,还是 shutdown 了写 。但有一点可以区分,如果对端是 close 了套接字,则本端在套接字上发送数据时,本端会收到对端发来的 RST 报文从而本端会触发 EPOLLIN + EPOLLRDHUP + EPOLLHUP + EPOLLERR 事件;而如果对端只是 shutdown 了写,则本端可以正常发送数据不会触发任何信号。
  • 当关闭(close)套接字时,内核会自动将套接字描述符从 epoll 中删除,因此本端不会再触发任何事件
文章作者: 极简
版权声明: 本站所有文章除特别声明外,均采用 CC BY-NC-SA 4.0 许可协议。转载请注明来自 后端技术分享
Socket网络编程
喜欢就支持一下吧