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

时间:2009-04-27 14:06:46

标签: python epoll

这是我的服务器

"""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仍然可以轮询recv并发送事件!为什么EPOLLERR事件永远不会发生?如果你使用EPOLLHUP也是一样。

我注意到EPOLLERR事件仅在您尝试编写已关闭的连接时发生。 除此之外,还有另一种方法可以判断连接是否已关闭?

如果在EPOLLIN事件中什么都没有,将连接视为已关闭是否正确?

10 个答案:

答案 0 :(得分:3)

EPOLLERR和EPOLLHUP从未在帖子中粘贴的代码中发生,因为它们总是与EPOLLIN或EPOLLOUT一起出现(其中一些可以一次设置),所以if / then / else总是拿起EPOLLIN或EPOLLOUT。

试验我发现EPOLLHUP只与EPOLLERR一起发生,其原因可能是python与epoll和低级IO接口的方式,通常recv会返回-1并在没有任何可用的情况下将errno设置为EAGAIN。一个非阻塞的recv,但是python使用''(没有返回任何内容)来表示EOF。

关闭你的telnet会话只会关闭tcp-connection的那一端,所以调用你身边的recv仍然是完全有效的,你的应用程序尚未读取的tcp接收缓冲区中可能有待处理的数据,以便不会触发错误条件。

似乎EPOLLIN和一个返回空字符串的recv表示另一端关闭了连接,但是,使用较旧版本的python(在引入epoll之前)和管道上的普通选择,我已经经历过返回的读数''并未表明EOF缺乏可用数据。

答案 1 :(得分:2)

如果套接字仍然打开但没有可用的读/写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)

答案 2 :(得分: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:

答案 3 :(得分:0)

您是否只需要将面具组合在一起以同时使用EPOLLHUP和EPOLLIN:


epoll.register(sk.fileno(), select.EPOLLIN | select.EPOLLHUP)

虽然说实话我并不熟悉epoll库,所以这只是一个建议......

答案 4 :(得分:0)

将select.EPOLLHUP处理代码移到select.EPOLLIN之前的行之后,hup事件仍然存在 不能进入'telnet'。但巧合的是,我发现如果我使用自己的客户端脚本,那就是 是hup事件!奇怪...

根据man epoll_ctl

   EPOLLRDHUP (since Linux 2.6.17)
          Stream socket peer closed connection, or shut down writing half of connection.  (This flag is especially useful for writing simple code  to
          detect peer shutdown when using Edge Triggered monitoring.)

   EPOLLERR
          Error  condition  happened on the associated file descriptor.  epoll_wait(2) will always wait for this event; it is not necessary to set it
          in events.

   EPOLLHUP
          Hang up happened on the associated file descriptor.  epoll_wait(2) will always wait for this event; it  is  not  necessary  to  set  it  in
          events.

当远程端关闭连接时,似乎有一个EPOLLRDHUP事件,这是python没有实现的,不知道为什么

答案 5 :(得分:0)

EPOLLRDHUP 标志没有在Python中定义。如果您的Linux内核是&gt; = 2.6.17,您可以定义它并在epoll中注册您的套接字,如下所示:

import select
if not "EPOLLRDHUP" in dir(select):
    select.EPOLLRDHUP = 0x2000
...
epoll.register(socket.fileno(), select.EPOLLIN | select.EPOLLRDHUP)

然后,您可以使用相同的标志( EPOLLRDHUP )捕获所需的事件:

elif event & select.EPOLLRDHUP:
     print "Stream socket peer closed connection"
     # try shutdown on both side, then close the socket:
     socket.close()
     epoll.unregister(socket.fileno())

有关详细信息,请查看python存储库中的selectmodule.c

答案 6 :(得分:0)

我有另一种方法..

try:
    data = s.recv(4096)
except socket.error:
    if e[0] in (errno.EWOULDBLOCK, errno.EAGAIN): # since this is a non-blocking socket..
        return # no error
    else:
        # error
        socket.close()

if not data: #closed either
    socket.close() 

答案 7 :(得分:0)

if event & select.EPOLLHUP:
    epoll.unregister(fd)

答案 8 :(得分:0)

elif event & (select.EPOLLERR | select.EPOLLHUP):
    epoll.unregister(fileno)
    cs[fileno].close()
    del cs[fileno]

答案 9 :(得分:0)

您在代码中未检测到EPOLLHUP / EPOLLERR的原因是您正在执行的按位操作。查看套接字何时准备读取epoll将抛出一个标志,其中第1位等于select.EPOLLIN(select.EPOLLIN == 1)。现在说客户端挂起(优雅与否)epoll在服务器上会抛出一个25位的标志,等于EPOLLIN + EPOLLERR + EPOLLHUP。因此,使用位25(代码中的事件变量),您可以看到如何检测EPOLLERR,因为所有elif语句(EPOLLOUT行除外)都不返回0,因此执行第一个elif语句,例如:

>>> from select import EPOLLIN,EPOLLOUT,EPOLLHUP,EPOLLERR
>>> event = 25
>>> event & EPOLLIN
1
>>> event & EPOLLERR
8
>>> event & EPOLLHUP
16
>>> event & EPOLLOUT
0

注意前三个如何不返回0?这就是为什么你的代码没有正确检测EPOLLERR / EPOLLHUP的原因。当一个客户端挂起时,你仍然可以从套接字中读取服务器端仍在运行(当然如果你这样做会返回0数据)因此EPOLLIN但是因为客户端挂起它也是EPOLLHUP因为它是EPOLLHUP所以它也是EPOLLERR挂断有些错误。我知道我对此发表评论的时间已经很晚了,但我希望我帮助那里的人大声笑

以下是我重写代码以表达我所说的更好的方法:

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()
read_only = select.EPOLLIN | select.EPOLLPRI | select.EPOLLHUP | select.EPOLLERR
read_write = read_only | select.EPOLLOUT
biterrs = [25,24,8,16,9,17,26,10,18] #Bitwise error numbers
epoll.register(s.fileno(),read_only)

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(),read_only)

        elif (event is select.EPOLLIN) or (event is select.EPOLLPRI):
            data = cs[fileno].recv(4)
            print 'recv ', data
            epoll.modify(fileno, read_write)
        elif event is select.EPOLLOUT:
            print 'send ', data
            cs[fileno].send(data)
            data = ''
            epoll.modify(fileno, read_only)

        elif event in biterrs:
            print 'err'
            epoll.unregister(fileno)