python 网络编程(二):IO 多路复用

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) 是不可省略的

  • 0
    点赞
  • 0
    收藏
    觉得还不错? 一键收藏
  • 0
    评论
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值