python 网络编程(二):IO 多路复用
注:本文使用 python 版本为 2.6.6,环境为 CentOS 6.7
IO 多路复用
日常的服务器不会在同一时间只处理一个客户端的请求,当有多个客户端同时连接时,就需要用到 IO 多路复用,在 C 中,如:Linux 下的 epoll,UNIX 下的 select/poll,freebsd 下的 kqueue
在 python 下也提供了 IO 多路复用的模块 select,模块导入:
import select
查看 select 模块帮助:
help(select)
select 模块支持 C 中常用的 IO 复用,如:
- select:在 windows,Unix 和 Linux 下均可使用,但在 windows 下,select 只能用于处理 socket
- poll:在 windows 下不可用
- epoll:只有Linux 2.5.44 以上版本支持
- kqueue:只有 BSD 系统支持
- kevent:只有 BSD 系统支持
如果考虑可移植性,建议使用 select,如果考虑性能,建议在 Linux 下使用 epoll
select 模块异常
select 相关模块异常会抛出 select.error
select
原型:
select(rlist, wlist, xlist[, timeout]) -> (rlist, wlist, xlist)
rlist 用于监控 IO 可读列表,如接受外部连接,从一个已有连接上接收到数据
wlist 用于监控 IO 可写列表,当 socket 可以写入,会返回,一般情况下可以不设置,或者在写人数据过多,socket 写缓冲区满的情况下可以监控写 IO,一旦 socket 重新准备完毕,再次写入
xlist 用于监控 IO 异常列表,如接受外部连接,从一个已有连接上接收到数据
timeout 为 select 阻塞超时时长,单位 s,不填表示一直阻塞,设为 0 表示不阻塞,设为正数表示阻塞的时间,可设置为浮点数,如 0.05 表示阻塞 50ms,如果 50ms 内没有任何 IO 事件发生,则退出 select
实例:
以下为一个使用 select 的回射服务端实例
#!/usr/bin/env python
import sys, socket, traceback, select
def init_connection(sock):
csock, caddr = sock.accept()
sys.stdout.write("recv connection from %s:%d\n" % (caddr))
return csock
def recv_data(recvev, csock):
buf = csock.recv(2048)
sys.stdout.write("recv data: %s\n" % buf)
if not len(buf):
csock.close()
recvev.remove(csock)
return
send_reply(csock, buf)
def send_reply(csock, buf):
reply = buf
csock.sendall(reply)
def printrecvev(recvev):
for ev in recvev:
print str(ev.fileno()) + '\n'
host = ''
port = ''
if len(sys.argv) == 2:
port = sys.argv[1]
elif len(sys.argv) == 3:
host = sys.argv[0]
port = sys.argv[1]
else:
sys.stdout.write("Usage: %s [host] port\n" % sys.argv[0])
sys.stdout.write("Example: %s 2539\n" % sys.argv[0])
sys.stdout.write("Example: %s 127.0.0.1 2539\n" % sys.argv[0])
sys.exit(1)
try:
port = int(port)
except ValueError:
try:
port = socket.getservbyname(port, 'tcp')
except:
sys.stdout.write("Cannot translate %s to port\n" % sys.argv[2])
sys.exit(1)
try:
sock = socket.socket(socket.AF_INET, socket.SOCK_STREAM)
sock.setblocking(False)
sock.setsockopt(socket.SOL_SOCKET, socket.SO_REUSEADDR, 1)
sock.bind((host, port))
sock.listen(5)
except socket.error, e:
sys.stdout.write("Error when creating socket server: %s\n" % e)
sys.exit(1)
recvev = [sock]
while True:
try:
r, w, e = select.select(recvev, [], [], 0.05)
for ev in r:
if ev == sock:
csock = init_connection(ev)
recvev.append(csock)
else:
recv_data(recvev, ev)
except KeyboardInterrupt:
sys.stdout.write("KeyboardInterrupt\n")
raise
except:
traceback.print_exc()
sock.close()
epoll
select.epoll
原型:
select.epoll([sizehint=-1]) -> epoll
select.epoll 是一个类方法,返回一个 epoll 对象,这里的参数可以为 -1 或 一个正数,只是 epoll 内部结构优化使用的参数,一般使用默认即可
实例:
ep = select.epoll()
epoll.register
原型:
register(fd[, eventmask])
epoll.register 是一个实例方法,用于在 epoll 中注册新的文件描述符(按 help 中的说法还可以修改已注册的文件,实际测试情况,如果 register 已经存在的文件描述符,会抛出异常),eventmask 不填默认为 EPOLL_IN|EPOLL_OUT|EPOLL_PRI
需要注意,这里的 fd 是文件描述符,实测这里是可以注册 socket 对象的,但是select.poll 中返回的是该 socket 对象对应的文件描述符,modify 和 unregister 与此相同
官方帮助文档中对于 event 事件的说明
- EPOLLIN Available for read
- EPOLLOUT Available for write
- EPOLLPRI Urgent data for read
- EPOLLERR Error condition happened on the assoc. fd
- EPOLLHUP Hang up happened on the assoc. fd
- EPOLLET Set Edge Trigger behavior, the default is Level Trigger behavior
- EPOLLONESHOT Set one-shot behavior. After one event is pulled out, the fd is internally disabled
- EPOLLRDNORM Equivalent to EPOLLIN
- EPOLLRDBAND Priority data band can be read.
- EPOLLWRNORM Equivalent to EPOLLOUT
- EPOLLWRBAND Priority data may be written.
- EPOLLMSG Ignored.
实例:
ep.register(sock)
epoll.modify
原型:
modify(fd, eventmask)
epoll.modify 是一个实例方法,用于在 epoll 中修改一个已注册的文件描述符,可以用 register 代替。eventmask 说明参考 epoll.register
实例:
ep.modify(sock)
epoll.unregister
原型:
unregister(fd)
epoll.unregister 是一个实例方法,用于在 epoll 中删除一个已注册的文件描述符
实例:
ep.fileno(sock)
epoll.poll
原型:
poll([timeout=-1[, maxevents=-1]]) -> [(fd, events), (...)]
epoll.poll 是一个实例方法,用于等待 IO 事件并返回一个已就绪 IO 事件的列表,fd 表示已就绪 IO 的文件描述符,events 表示已就绪 IO 的类型。timeout 为 poll 阻塞事件,单位为 s,-1 表示一直阻塞,0 表示不阻塞,如阻塞 50ms,可设置为 0.05。maxevents 表示最多返回的事件数,-1 表示无限制
这里的 fd 是文件描述符,如果注册的为 socket 对象如 sock,这里的 fd 为 sock.fileno()
实例:
fd = ep.fileno()
epoll.close
原型:
epoll.close()
epoll.close 是一个实例方法,用于关闭 epoll
实例:
ep = select.close()
epoll.fileno
原型:
fileno() -> int
epoll.fileno 是一个实例方法,用于获取 epoll 的文件描述符
实例:
fd = ep.fileno()
常用的 eventmask 值说明
EPOLLIN
当服务端收到客户端连接建立请求时会触发 EPOLLIN 事件
当从 socket 上收到对端数据时会触发 EPOLLIN 事件
当客户端断开连接,服务端会受到一个 EPOLLIN 事件
EPOLLOUT
当客户端向服务端建立 socket 连接成功时会触发 EPOLLOUT 事件
当socket 写缓冲区从不可写入变为可写入时会触发 EPOLLOUT 事件
ET 和 LT 触发
epoll 默认为 LT 触发模式,该模式下,只要有 IO 事件没处理,就会通知用户:
- 读事件:用户在读取 socket 缓冲区,如果一次没有读完,下次进到 select.poll,select.poll 会立即返回一个 EPOLLIN 事件,用户可以继续读取上次没读完的缓冲区
- 写事件:当 socket 缓冲区可写时就会返回,这样会存在一个问题,如果用户注册了 EPOLLOUT,socket 不关闭的情况下,每次调用 select.poll,都会返回 EPOLLOUT 事件。所以在使用 LT 模式时,如果不需要写数据,要把注册的 EPOLLOUT 删除
ET 为边缘触发,使用该模式,须在注册时 eventmask 包含EPOLLET,该模式下只会在事件发生时通知一次:
- 读事件:用户如果一次没有从 socket 缓冲区读完数据,select.poll 不会再进行通知,用户只有下次有新数据时才能继续读取
- 写事件:当 socket 可写时会通知一次,如客户端建立连接成功时。或者缓冲区从不可写入变得可写入时
实例
LT模式实例
以下为一个 LT 模式的实例,这个例子中我们在收到消息后,把消息放到了 sock 对应的缓冲区中,然后设定了 EPOLLOUT 事件,这样 select.poll 调用 send_reply 将缓冲区的消息回送回客户端
#!/usr/bin/env python
import sys, socket, traceback, select
global evlists
evlists = {}
def isEvent(event, events):
return event == event & events
'''
evlist[sock, eventmask, rhandler, whandler, buf]
'''
def addevent(epfd, sock, event, handler):
print "in addevent"
evlist = evlists.get(sock.fileno())
newe = False
if evlist == None:
newe = True
evlist = [sock, 0, None, None, ""]
if event == select.EPOLLIN:
evlist[1] |= select.EPOLLIN | select.EPOLLHUP
evlist[2] = handler
else:
evlist[1] |= select.EPOLLOUT
evlist[3] = handler
if newe:
epfd.register(sock.fileno(), evlist[1])
else:
epfd.modify(sock.fileno(), evlist[1])
evlists[sock.fileno()] = evlist
def delevent(epfd, sock, event):
print "in delevent"
evlist = evlists.get(sock.fileno())
if evlist == None:
return
old = evlist[1]
if isEvent(select.EPOLLET, old):
evlist[1] = select.EPOLLET
else:
evlist[1] = 0
if select.EPOLLIN == event:
if isEvent(select.EPOLLOUT, old):
evlist[1] |= select.EPOLLOUT
evlist[2] = None
elif select.EPOLLOUT == event:
if isEvent(select.EPOLLOUT, old):
evlist[1] |= select.EPOLLIN | select.EPOLLHUP
evlist[3] = None
if not (isEvent(select.EPOLLIN | select.EPOLLHUP, evlist[1]) or isEvent(select.EPOLLOUT, evlist[1])):
epfd.unregister(evlist[0].fileno())
del evlists[sock.fileno()]
else:
epfd.modify(evlist[0].fileno(), evlist[1])
evlists[sock.fileno()] = evlist
def triggerevent(epfd):
print "in triggerevent"
elist = epfd.poll()
for e in elist:
ev = evlists.get(e[0])
if None == ev:
return
if isEvent(select.EPOLLIN, e[1]):
if None != ev[2]:
ev[2](epfd, ev[0])
if isEvent(select.EPOLLOUT, e[1]):
if None != ev[3]:
ev[3](epfd, ev[0])
def init_connection(epfd, sock):
print "in init_connection"
csock, caddr = sock.accept()
sys.stdout.write("recv connection from %s:%d\n" % (caddr))
addevent(epfd, csock, select.EPOLLIN, recv_data)
def recv_data(epfd, sock):
print "in recv_data"
buf = sock.recv(2048)
sys.stdout.write("recv data[%d]: %s\n" % (len(buf), buf))
if not len(buf):
delevent(epfd, sock, select.EPOLLIN | select.EPOLLOUT)
sock.close()
return
evlists[sock.fileno()][4] = buf
addevent(epfd, sock, select.EPOLLOUT, send_reply)
def send_reply(epfd, sock):
print "in send_reply"
reply = evlists[sock.fileno()][4]
sock.sendall(reply)
delevent(epfd, sock, select.EPOLLOUT)
host = ''
port = ''
if len(sys.argv) == 2:
port = sys.argv[1]
elif len(sys.argv) == 3:
host = sys.argv[0]
port = sys.argv[1]
else:
sys.stdout.write("Usage: %s [host] port\n" % sys.argv[0])
sys.stdout.write("Example: %s 2539\n" % sys.argv[0])
sys.stdout.write("Example: %s 127.0.0.1 2539\n" % sys.argv[0])
sys.exit(1)
try:
port = int(port)
except ValueError:
try:
port = socket.getservbyname(port, 'tcp')
except:
sys.stdout.write("Cannot translate %s to port\n" % sys.argv[2])
sys.exit(1)
try:
sock = socket.socket(socket.AF_INET, socket.SOCK_STREAM)
sock.setblocking(False)
sock.setsockopt(socket.SOL_SOCKET, socket.SO_REUSEADDR, 1)
sock.bind((host, port))
sock.listen(5)
except socket.error, e:
sys.stdout.write("Error when creating socket server: %s\n" % e)
sys.exit(1)
ep = select.epoll()
addevent(ep, sock, select.EPOLLIN, init_connection)
while True:
try:
triggerevent(ep)
except KeyboardInterrupt:
sys.stdout.write("KeyboardInterrupt\n")
raise
except:
traceback.print_exc()
sock.close()
ET模式实例
ET 模式只需把上例中的
if evlist == None:
newe = True
evlist = [sock, 0, None, None, ""]
改为
if evlist == None:
newe = True
evlist = [sock, select.EPOLLET, None, None, ""]
ET 模式下在连接进来后,recv_data 时,注册 EPOLLOUT 时,内核会通知一次状态可写,此时会调用 send_reply,如果 send_reply 不取消 EPOLLOUT 的注册,socket 缓冲区一直处于可写状态,没有发生从不可写到可写的状态切换,后续将不会再触发 EPOLLOUT,因此永远不会调用 send_reply
因此此处 send_reply 中的 delevent(epfd, sock, select.EPOLLOUT) 是不可省略的