epoll如何在Python中检测客户端关闭?

5 投票
10 回答
11419 浏览
提问于 2025-04-15 11:16

这是我的服务器

"""Server using epoll method"""

import os
import select
import socket
import time

from oodict import OODict

addr = ('localhost', 8989)

s = socket.socket(socket.AF_INET, socket.SOCK_STREAM)
s.setsockopt(socket.SOL_SOCKET, socket.SO_REUSEADDR, 1)
s.bind(addr)
s.listen(8)
s.setblocking(0) # Non blocking socket server
epoll = select.epoll()
epoll.register(s.fileno(), select.EPOLLIN) # Level triggerred

cs = {}
data = ''
while True:
    time.sleep(1)
    events = epoll.poll(1) # Timeout 1 second
    print 'Polling %d events' % len(events)
    for fileno, event in events:
        if fileno == s.fileno():
            sk, addr = s.accept()
            sk.setblocking(0)
            print addr
            cs[sk.fileno()] = sk
            epoll.register(sk.fileno(), select.EPOLLIN)

        elif event & select.EPOLLIN:
            data = cs[fileno].recv(4)
            print 'recv ', data
            epoll.modify(fileno, select.EPOLLOUT)
        elif event & select.EPOLLOUT:
            print 'send ', data
            cs[fileno].send(data)
            data = ''
            epoll.modify(fileno, select.EPOLLIN)

        elif event & select.EPOLLERR:
            print 'err'
            epoll.unregister(fileno)

客户端输入

ideer@ideer:/home/chenz/source/ideerfs$ telnet localhost 8989
Trying ::1...
Trying 127.0.0.1...
Connected to localhost.
Escape character is '^]'.
123456
123456
^]

telnet> q
Connection closed.

服务器端输出

ideer@ideer:/chenz/source/ideerfs$ python epoll.py 
Polling 0 events
Polling 0 events
Polling 1 events
('127.0.0.1', 53975)
Polling 0 events
Polling 1 events
recv  1234
Polling 1 events
send  1234
Polling 1 events
recv  56

Polling 1 events
send  56

Polling 0 events
Polling 0 events
Polling 0 events
Polling 1 events
recv  
Polling 1 events
send  
Polling 1 events
recv  
Polling 1 events
send  
Polling 1 events
recv  
Polling 1 events
send  
Polling 1 events
recv  
^CTraceback (most recent call last):
  File "epoll.py", line 23, in <module>
    time.sleep(1)
KeyboardInterrupt

奇怪的是,当客户端关闭连接后,epoll 仍然可以检测到接收和发送事件!为什么 EPOLLERR 事件从来不会发生?使用 EPOLLHUP 也是一样的情况。

我注意到,EPOLLERR 事件只有在你尝试向一个已经关闭的连接写入数据时才会发生。除此之外,还有其他方法可以判断连接是否已经关闭吗?

如果在 EPOLLIN 事件中什么都没有收到,认为连接已经关闭,这样做是否正确?

10 个回答

1

我临时想出的一个解决办法来绕过这个问题

--- epoll_demo.py.orig  2009-04-28 18:11:32.000000000 +0800
+++ epoll_demo.py   2009-04-28 18:12:56.000000000 +0800
@@ -18,6 +18,7 @@
 epoll.register(s.fileno(), select.EPOLLIN) # Level triggerred

 cs = {}
+en = {}
 data = ''
 while True:
     time.sleep(1)
@@ -29,10 +30,18 @@
             sk.setblocking(0)
             print addr
             cs[sk.fileno()] = sk
+            en[sk.fileno()] = 0
             epoll.register(sk.fileno(), select.EPOLLIN)

         elif event & select.EPOLLIN:
             data = cs[fileno].recv(4)
+            if not data:
+                en[fileno] += 1
+                if en[fileno] >= 3:
+                    print 'closed'
+                    epoll.unregister(fileno)
+                continue
+            en[fileno] = 0
             print 'recv ', data
             epoll.modify(fileno, select.EPOLLOUT)
         elif event & select.EPOLLOUT:
2

如果这个连接(socket)还在打开状态,但没有可以读或写的数据,epoll.poll 就会超时。

如果对方有数据发送过来,你会收到一个 EPOLLIN 的信号,这时候就可以读取到数据了。

如果对方关闭了连接,你会收到一个 EPOLLIN 的信号,但当你尝试读取时,会得到一个空字符串 ""。

这时你可以通过关闭这个连接来处理,关闭后会收到一个 EPOLLHUP 的事件,这样可以清理你内部的数据结构。

或者你也可以进行清理工作,然后注销这个 epoll。

elif event & select.EPOLLIN:
    data = cs[fileno].recv(4)

if not data:
    epoll.modify(fileno, 0)
    cs[fileno].shutdown(socket.SHUT_RDWR)
5

在帖子中提到的代码里,EPOLLERR和EPOLLHUP这两个情况从来没有出现过,原因是它们通常会和EPOLLIN或EPOLLOUT一起出现(这些状态可以同时被设置),所以在if/then/else判断中,总是能捕捉到EPOLLIN或EPOLLOUT。

通过实验,我发现EPOLLHUP通常是和EPOLLERR一起出现的。这可能是因为Python与epoll和底层输入输出的接口方式。一般来说,当在非阻塞的recv中没有数据可用时,recv会返回-1,并把errno设置为EAGAIN,而Python则用''(返回空)来表示文件结束(EOF)。

关闭你的telnet会话只会关闭TCP连接的一端,所以在你这边继续调用recv是完全有效的,因为TCP接收缓冲区里可能还有你的应用程序还没读取的数据,这样不会触发错误。

看起来EPOLLIN和recv返回空字符串意味着另一端已经关闭了连接。不过,我在使用较旧版本的Python(在引入epoll之前)和普通的select在管道上时,发现返回''并不表示EOF,只是没有可用的数据。

撰写回答