select是通过系统调用来监视着一个由多个文件描述符组成的数组,当select()返回后,数组中就绪的文件描述符会被内核修改标记位,使得进程可以获得这些文件描述符从而进行后续的读写操作。select是通过遍历来监视整个数组的,而且每次遍历都是线性的。
select目前几乎在所有的平台上支持,良好的跨平台性。
调用select的函数为r,w,e = select.select(rlist,wlist,xlist[, timeout]),前三个参数都分别是三个列表,数组中的对象均为waitable object:均是整数的文件描述符(file descriptor)或者 一个拥有返回文件描述符方法fileno()的对象:
select方法用来监视文件描述符,如果文件描述符发生变化,则获取该描述符。
服务器代码
import select
import socket
from queue import Queue
from time import sleep
server = socket.socket(socket.AF_INET,socket.SOCK_STREAM)
server.setblocking(False)
server_address = ("localhost",8090)
print("starting up on %s port %s" % server_address)
server.bind(server_address)
server.listen(5)
inputs = [server]
outputs = []
message_queues= {}
while inputs:
print("waiting for the next event.")
readable,writable,exceptional = select.select(inputs, outputs, inputs)
# 循环判断是否由客户端连接进来,当有客户端连接进来时select将触发
for s in readable:
# 判断当前触发的是不是服务端对象,当触发的对象服务端对象,说明有新客户端连接上来了
# 表示有新用户连接
if s is server:
connection,client_address = s.accept()
print("connection from",client_address)
connection.setblocking(0)
# 将客户端对象也加入到监听的列表中,当客户端发送消息时 select将触发
inputs.append(connection)
# 为连接的客户端单独创建一个消息队列,用来保存客户端发送的消息
message_queues[connection] = Queue()
else:
# 有老用户发消息,处理
data = s.recv(1024)
# 客户端未断开
if data != b"":
print("received %s from %s"%(data,s.getpeername()))
message_queues[s].put(data)
if s not in outputs:
outputs.append(s)
else:
if s in outputs:
outputs.remove(s)
inputs.remove(s)
s.close()
del message_queues[s]
for s in writable:
try:
message_queue = message_queues.get(s)
send_data = ""
if message_queue is not None:
send_data = message_queue.get_nowait()
else:
print("客户端断开了")
except Queue.empty():
print("客户端断开了")
outputs.remove(s)
else:
if message_queue is not None:
s.send(send_data)
else:
print("hash closed")
# 处理异常情况
for s in exceptional:
inputs.remove(s)
if s in outputs:
outputs.remove(s)
s.close()
del message_queues[s]
sleep(1)
客户端代码
import socket
messages = ['This is the message ', 'It will be sent ', 'in parts ', ]
server_address = ('localhost', 8090)
# Create aTCP/IP socket
socks = [socket.socket(socket.AF_INET, socket.SOCK_STREAM), socket.socket(socket.AF_INET, socket.SOCK_STREAM), ]
# Connect thesocket to the port where the server is listening
print ('connecting to %s port %s' % server_address)
# 连接到服务器
for s in socks:
s.connect(server_address)
for index, message in enumerate(messages):
# Send messages on both sockets
for s in socks:
print ('%s: sending "%s"' % (s.getsockname(), message + str(index)))
s.send(message.encode('utf8'))
# Read responses on both sockets
for s in socks:
data = s.recv(1024)
print ('%s: received "%s"' % (s.getsockname(), data))
if data != "":
print ('closingsocket', s.getsockname())
s.close()
poll本质上和select没有区别,只是没有了最大连接数(linux上默认1024个)的限制,原因是它基于链表存储的。
poll除了没有最大连接数的缺点,其他都和select一样。
import select
def create_srv_socket(address):
'''建立并返回一个服务器监听socket'''
listener = socket.socket(socket.AF_INET,socket.SOCK_STREAM)
listener.setsockopt(socket.SOL_SOCKET,socket.SO_REUSEADDR,1)
listener.bind(address)
listener.listen(64)
print("Listening at {}".format(address))
return listener
def all_events_forever(poll_object):
while True:
for fd,event in poll_object.poll():
yield fd,event
def serve(listener):
sockets = {listener.fileno():listener} # 创建一个字典保存所有的sock
addresses = {}
bytes_received = {} # 缓存接受的数据
bytes_to_send = {} # 缓存发送的数据
poll_object = select.poll()
poll_object.register(listener,select.POLLIN) # 注册一个读取的poll对象
for fd,event in all_events_forever(poll_object): # 利用生成器,去读描述符和事件
sock = socket[fd] # 取得sock
if event & (select.POLLHUB |select.POLLERR | select.POLLNVAL) # 如果事件是挂起、错误或无效请求,则删除
address = addresses.pop(sock)
rb = bytes_received.pop(sock,b"")
sb = bytes_to_send.pop(sock,b"")
if rb:
print("Client {} send {} but then closed".format(address,rb))
elif sb:
print("Client {} closed before we send {}".format(address,sb))
else:
print("Client {} closed socket normally".format(address))
poll_object.unregister(fd) # 注销一个fd
del sockets[fd]
elif sock is listener: # 如果有监听者事件,说明有新的客户端连接
sock,address = sock.accept()
print("Accepted connection from {}".format(address))
sock.setblocking(False)
sockets[sock.fileno()] = sock
address[sock] = address
poll_object.register(sock,select.POLLIN)
elif event & select.POLLIN: # 如果有读事件
more_data = sock.recv(4096)
if not mort_data: # 数据读完了
sock.close()
continue
data = bytes_received.pop(sock,b"") +more_data
if data.endswith(b"?"):
bytes_to_send[sock] = data
poll_object.modify(sock,select.POLLOUT) # 读完了改成写
else:
bytes_received[sock] = data
elif event & select.POLLOUT:
data = bytes_to_send.pop(sock)
n = sock.send(data)
if n <len(data):
bytes_to_send[sock] = data[n:]
else:
poll_object.modify(sock,select.POLLIN)
if __main__ == "__main__":
address =
listener = create_srv_socket(address)
server(listener)
在linux由内核直接支持的方法。epoll解决了select和poll的缺点。
epoll同时支持水平触发和边缘触发:
python中的DefaultSelector库,该模块封装了高效的I/O多路复用,该模块根据使用的平台,选择该平台下最高效的I/O多路复用实现(select()、poll()、epoll())
该例子可以让我们更清楚的理解什么叫事件循环,下述代码中,我们在初始化中创建了一个非阻塞的socket,将其与目标服务器连接后,就紧接着为该socket注册了一个 写事件 的回调函数。在 写事件的回调函数中,我们注册了一个读事件的回调函数。我认为最关键的就在于如何理解loop函数,selector.select()函数是一个阻塞函数,当注册的socket有活跃的响应,才会返回值,这样,我们就可以遍历返回值,找到活跃的socket调用响应的回调函数。利用这里例子,尝试写一个聊天室的服务器。
import socket
from selectors import DefaultSelector, EVENT_READ, EVENT_WRITE
from urllib.parse import urlparse
selector = DefaultSelector()
stop = False
class Fetcher:
def __init__(self,url):
self.url = url
self.url = urlparse(self.url)
self.host = self.url.netloc
self.path = self.url.path
self.data = b""
if self.path == "":
self.path = "/"
self.client = socket.socket(socket.AF_INET,socket.SOCK_STREAM)
self.client.setblocking(False)
try:
self.client.connect((self.host,80))
except BlockingIOError as e:
pass
selector.register(self.client.fileno(),EVENT_WRITE,self.connected)
def connected(self,key):
selector.unregister(key.fd)
try:
self.client.send("GET {} HTTP/1.1\r\nHost:{}\r\nConnection:close\r\n\r\n".format(self.path,self.host).encode("utf8"))
except BrokenPipeError as e:
pass
selector.register(self.client.fileno(),EVENT_READ,self.readable)
def readable(self,key):
d = self.client.recv(1024)
if d:
self.data += d
else:
selector.unregister(key.fd)
data = self.data.decode("utf8")
html_data = data.split("\r\n\r\n")[1]
print(html_data)
self.client.close()
global stop
stop = True
def loop():
# 模拟了一个事件循环,注意事件循环需要我们自己实现,而不是操作系统去实现。
while not stop:
ready = selector.select()
for key,mask in ready:
call_back = key.data
call_back(key)
if __name__ == '__main__':
f = Fetcher("http://www.baidu.com")
loop()
下面我们尝试实现一个聊天室的服务器
# encoding:utf8
# 聊天服务器,基于selector
# 简单描述逻辑
# 1. 拥有loop函数,事件循环
# 2. 初始化selecotr,socket,已存在的描述符集合
import socket
from selectors import DefaultSelector, EVENT_READ, EVENT_WRITE
class chatServer:
def __init__(self):
self.selector = DefaultSelector()
self.socketFds = []
self.serverSocket = socket.socket(socket.AF_INET, socket.SOCK_STREAM) # 创建服务器socket
self.serverSocket.setsockopt(socket.SOL_SOCKET, socket.SO_REUSEADDR, 1)
self.serverSocket.setblocking(False) # 设置服务器socket为非阻塞
self.serverSocket.bind(("localhost", 10000)) # 设置服务器socket监听("127.0.0.1",8001)
self.serverSocket.listen(10)
self.selector.register(self.serverSocket, EVENT_READ, self.serverSocketAccept) # 为服务器socket注册读事件的回调函数
# self.selector.register(self.serverSocket.fileno(),EVENT_WRITE,self.serverSocketWrite) # 为服务器socket注册读事件的回调函数
self.clientData = {} # 每一个socket对应一个消息缓冲区,当
self.nameClient = {} # 每一个socket对应一个用户名
self.loop()
def loop(self):
while True:
ready = self.selector.select()
for key, mask in ready:
call_back = key.data
call_back(key.fileobj, mask)
def serverSocketAccept(self, sock, mask):
'''
当服务器socket可读时,说明有新的客户端连接上了。
'''
conn, addr = self.serverSocket.accept() # 新连接
conn.send(b"what's your name?")
conn.setblocking(False)
self.clientData[conn] = [] # 每一个socket对应一个消息存储区
self.nameClient[conn] = "" # 每个socket对应一个用户名
self.socketFds.append(conn) # 添加新的socket
self.selector.register(conn, EVENT_READ, self.clientRead)
def clientRead(self, conn, mask):
data = conn.recv(2) # 从客户端的socket读取数据
self.clientData[conn].append(data.decode('utf8')) # 向消息存储区中存储数据
if data.endswith(b"\n"):
# 该socket读完了,从消息存储区中取得数据
message = "".join(self.clientData[conn])
self.clientData[conn].clear()
if not self.nameClient[conn]:
# 如果没有用户名
self.nameClient[conn] = message
else:
for sock in self.socketFds:
if sock != conn:
sock.send((self.nameClient[conn] + " say:" + message).encode('utf8'))
if __name__ == '__main__':
chatServer = chatServer()