在select模块中, 有三种方法实现IO多路复用并发服务器
- select
- poll
- epoll
select的原理: 在多路复用的模型中,比较常用的有select模型和epoll模型。这两个都是系统接口,由操作系统提供。当然,Python的select模块进行了更高级的封装。
网络通信被Unix系统抽象为文件的读写,通常是一个设备,由设备驱动程序提供,驱动可以知道自身的数据是否可用。支持阻塞操作的设备驱动通常会实现一组自身的等待队列,如读/写等待队列用于支持上层(用户层)所需的block或non-block操作。设备的文件的资源如果可用(可读或者可写)则会通知进程,反之则会让进程睡眠,等到数据到来可用的时候,再唤醒进程。
这些设备的文件描述符被放在一个数组中,然后select调用的时候遍历这个数组,如果对于的文件描述符可读则会返回改文件描述符。当遍历结束之后,如果仍然没有一个可用设备文件描述符,select让用户进程则会睡眠,直到等待资源可用的时候在唤醒,遍历之前那个监视的数组。每次遍历都是依次进行判断的。
例如使用select实现echo(回显)服务器
import select
import socket
import sys
server = socket.socket(socket.AF_INET, socket.SOCK_STREAM)
server.bind(('', 7788))
server.listen(5)
inputs = [server, sys.stdin]
running = True
while True:
# 调用 select 函数,阻塞等待
readable, writeable, exceptional = select.select(inputs, [], [])
# 数据抵达,循环
for sock in readable:
# 监听到有新的连接
if sock == server:
conn, addr = server.accept()
# select 监听的socket
inputs.append(conn)
# 监听到键盘有输入
elif sock == sys.stdin:
cmd = sys.stdin.readline()
running = False
break
# 有数据到达
else:
# 读取客户端连接发送的数据
data = sock.recv(1024)
if data:
sock.send(data)
else:
# 移除select监听的socket
inputs.remove(sock)
sock.close()
# 如果检测到用户输入敲击键盘,那么就退出
if not running:
break
server.close()
但是在底层原理中, select和epoll 都是使用轮询原理来实现的. epoll是触发通知机制
epoll的优点:
- 没有最大并发连接的限制,能打开的FD(指的是文件描述符,通俗的理解就是套接字对应的数字编号)的上限远大于1024
- 效率提升,不是轮询的方式,不会随着FD数目的增加效率下降。只有活跃可用的FD才会调用callback函数;即epoll最大的优点就在于它只管你“活跃”的连接,而跟连接总数无关,因此在实际的网络环境中,epoll的效率就会远远高于select和poll。
import socket
import select
# 创建套接字
s = socket.socket(socket.AF_INET, socket.SOCK_STREAM)
# 可重复绑定
s.setsockopt(socket.SOL_SOCKET, socket.SO_REUSEADDR, 1)
# 绑定本机地址端口
s.bind(("", 7788))
# 变为被动服务器
s.listen(1024)
# 创建一个epoll对象
epoll = select.epoll()
# 在epoll中注册s套接字(注意此处不是直接用是s, 而是使用s的fileno)
epoll.register(s.fileno(), select.EPOLLIN|select.EPOLLET)
# 创建两个字典, 来保存fileno和与其对应的套接字和地址
connections = {}
addresses = {}
# 开始等待客户端发送来的信息
while True:
# 对epoll中的套接字进行扫描
epollList = epoll.poll()
# 对扫描到的事件进行判断
for fd,events in epollList:
# 如果判断是s套接字
if fd == s.fileno():
conn,addr = s.accept()
print("有新的客户端到来...%s"%str(addr))
connections[conn.fileno()] = conn
addresses[conn.fileno()] = addr
epoll.register(conn.fileno(), selecte.EPOLLIN|select.EPOLLET)
# 如果是接收到了数据
elif events == select.EPOLLIN:
recvData = connections[fd].recv(1024)
if len(recvData) > 0:
print("recvData: %s"%recvData)
else:
epoll.unregister(fd)
connections[fd].close()
print("%s....offline....."%str(addresses[fd]))
值得注意的是,只有在linux2.6以上系统中才会有epoll, 在mac系统中是kqueue.