模块信息简介:
asyncore
Asyncore是python的标准库。文件asyncore.py中最重要的类为dispatcher,使用的时候只需要继承asyncore.dispatcher就好了
可以这样理解每一个继承了asyncore.dispatcher的类都代表了一个socket(监听socket或者连接socket)
实际上,asyncore.dispatcher就是对socket对象进行了简单的封装,这个socket就可以添加到一个全局的字典中,Asyncore模块中的loop函数会对这些socket文件描述符进行监视。asyncore同时提供了select.select、poll的封装(实际上现在大家都使用更为高效的epoll),
变成了框架的使用模式,通过I/O多路复用实现同时处理多个请求,避免使用多线程或者多进程的方式。该库已经作为兼容模式存在,新的库为asyncio。且在2和3中asyncore代码有一点点差异。
asyncore.dispatcher
Asyncore.dispatcher 是这个库中的一个socket的框架,为socket添加了一些通用的回调方法。
- 添加了4个状态,connected/accepting/connecting/closing。
connected:已经连接上对方,比如监听套接字sock,addr = socket.accept()返回的sock - add_channel/del_channel/create_socket/set_socket这几个都是针对全局字典socket_map的操作。添加进去或者删除
- readable/writable 用的地方不是太多,预先确定该socket该不该添加到socket的可读可写里面
- listen/bind/connect/accept/send/recv/close 这几个函数嘛,对原有的行为稍微改动了下,比如listen设置为accepting状态,connect设置为connecting状态,send,recv一定条件出发close,close即为从全局字典中删除并关闭socket
- handle_read_event/handle_connect_event/handle_write_event.当select有事件返回的时候就是调用的这3个方法。只是需要注意。这里并不是最终执行的操作(send、recv等)!!!这里也体现了标注socket状态的作用,read可以分为监听(accepting)、连接完成后(connecting)→→→这里就是一个hook,如果自己调用connect连接那么完成后是此状态,可以自己重写handle_connect处理该事件,如果是直接初始化的socket则直接为connected状态。handle_write_event也很明显,当为connecting状态的时候处理hook,否则处理写入事件
- 最后这一部分基本就是使用者需要重写的部分了。
handle_expt/handle_read/handle_write/handle_connect/handle_accept/handle_close含义很明白,就不表述了
这些方法是在异步循环中被调用的。否则它就是一个标准的非阻塞socket对象。
asyncore.dispatcher_with_send
dispatcher子类添加了简单的缓冲输出功能用于简单的客户,更复杂的使用asynchat.async_chat。
asyncore.file_dispatcher
file_dispatcher需要一个文件描述符或文件对象地图以及一个可选的参数,包装,使用调查()或循环()函数。如果提供一个文件对象或任何fileno()方法,该方法将调用和传递到file_wrapper构造函数。可用性:UNIX。
asyncore.file_wrapper
file_wrapper需要一个整数文件描述符并调用os.dup()复制处理,这样原来的处理可能是独立于file_wrapper关闭。这个类实现足够的方法来模拟一个套接字使用file_dispatcher类。可用性:UNIX。
asyncore.loop
输入一个轮询循环直到通过计数或打开的通道已关闭。
asynchat
这个模块建立在 asyncore 基础结构上,简化了异步客户端和服务器,使得处理带有以任意字符串终止或者可变长度的元素的协议更加容易。
asynchat 定义了你子类化的抽象类 async_chat ,提供了 collect_incoming_data() 和 found_terminator() 方法的实现。
它使用了和 asyncore一样的异步循环,和两种类型的通道, asyncore.dispatcher 和 asynchat.async_chat,它们可以被自由地混合在通道映射中。
当接收传入的连接请求时,一个 asyncore.dispatcher 服务器通道往往会产生新的 asynchat.async_chat 通道对象。
asynchat.async_chat
这个类是asyncore.dispatcher的抽象子类。一般使用其collect_incoming_data()和found_terminator()方法。
1. collect_incoming_data() - 接收数据并缓存。
2. found_terminator() - 当输入数据流符合由 set_terminator() 设置的终止条件时被调用。
3. set_terminator() - 设置终止条件。
4. push() - 向通道压入数据以确保其传输。
关于dispatcher和async_chat的选择
async_chat在dispatcher的基础上,只需指定接收数据时应该执行的操作以及发现终止符后如何响应。向通道压入的数据通过由async_chat管理的FIFO对象进行传输。从dispatcher的子类asyncore.file_dispatcher的描述中可以看出,async_chat提供了更加定制化的通信方式。如果继承async_chat作为sever发起监听socket就意味着我们需要将实现async_chat中的抽象函数,作为server来说显然是不必要的。
IO多路复用
阻塞IO和非阻塞IO
从程序编写的角度来看,I/O就是调用一个或多个系统函数,完成对输入输出设备的操作。输入输出设置可以是显示器、字符终端命令行、网络适配器、磁盘等。操作系统在这些设备与用户程序之间完成一个衔接,称为驱动程序,驱动程序向下驱动硬件,向上提供抽象的函数调用入口。
一般来说I/O操作是需要时间的,因为这涉及到系统、硬件等计算器模块的互相配合,所以必然不像普通的函数调用那样能够按照既定的方式立即返回。
从用户代码的角度,I/O操作的系统调用分为“阻塞”和“非阻塞”两种。
1. “阻塞”的调用会在I/O调用完成前,挂起调用线程,即CPU会不再执行后续代码,而是等到I/O完成后再回来继续执行,在用户代码看来,线程停止执行了,在调用处等待了。
2. “非阻塞”的调用则不同,I/O调用基本上是立即返回,而且往往实际上I/O此时并没有完成,所以需要用户的程序轮询结果。
我们以网络IO为例,看一下对于一个服务器,“阻塞”和“非阻塞”两种模式,该如何设计。由于服务器要同时服务多个客户端,所以需要同时操作多个Socket。
可以看到,如果使用阻塞的IO方式,因为每个Socket都会阻塞,为了同时服务多个客户端,需要多个线程同时挂起;而如果采用非阻塞的调用方式,则需要在一个线程中不断轮训每个客户端是否有数据到来。
显然纯粹阻塞式的调用不可取,非阻塞式的调用看起来不错,但是仍不够好,因为轮询实际也是通过某种系统调用完成的,相当于在用户空间进行的,效率不高,如果能够在内核空间进行这种类似轮询,然后让内核通知用户空间哪个IO就绪了,就更好了。于是引出接下来的概念:IO多路复用
IO多路复用
IO多路复用是一种系统调用,内核能够同时对多个IO描述符进行就绪检查。当所有被监听的IO都没有就绪时,调用将阻塞;当至少有一个IO描述符就绪时,调用将返回,用户代码可通过检查究竟是哪个IO就绪来进一步处理业务。显然,IO多路复用是解决系统里面存在N个IO描述符的问题的,这里必须明确IO复用和IO阻塞与否并不是一个概念,IO复用只检测IO是否就绪(读就绪或者写就绪等),具体的数据的输入输出还是需要依靠具体的IO操作完成(阻塞操作或非阻塞操作)。最典型的IO多路复用技术有select、poll、epoll等。select具有最大数量描述符限制,而epoll则没有,并且在机制上,epoll也更为高效。select的优势仅仅是跨平台支持性,所有平台和较低版本的内核都支持select模式,epoll则不是。
在IO相关的编程中,IO复用起到的作用相当于一个阀门,让后续IO操作更为精准高效。
编程模型
阻塞式。纯采用阻塞式,这种方式很少见,基本只会出现在demo中。多个描述符需要用多个进程或者线程来一一对应处理。
非阻塞式。纯非阻塞式,对IO的就绪与否需要在用户空间通过轮询来实现。
IO多路复用+阻塞式。仅使用一个线程就可以实现对多个描述符的状态管理,但由于IO输入输出调用本身是阻塞的,可能出现某个IO输入输出过慢,影响其他描述符的效率,从而体现出整体性能不高。此种方式编程难度比较低。
IO多路复用+非阻塞式。在多路复用的基础上,IO采用非阻塞式,可以大大降低单个描述符的IO速度对其他IO的影响,不过此种方式编程难度较高,主要表现在需要考虑一些慢速读写时的边界情况,比如读黏包、写缓冲不够等。
关于IO多路复用,这里以select方法为例
进程指定内核监听哪些文件描述符(最多监听1024个fd)的哪些事件,当没有文件描述符事件发生时,进程被阻塞;当一个或者多个文件描述符事件发生时,进程被唤醒。
当我们调用select()时:
上下文切换转换为内核态
将fd从用户空间复制到内核空间
内核遍历所有fd,查看其对应事件是否发生
如果没发生,将进程阻塞,当设备驱动产生中断或者timeout时间后,将进程唤醒,再次进行遍历
返回遍历后的fd
将fd从内核空间复制到用户空间
网络通信被Unix系统抽象为文件的读写,通常是一个设备,由设备驱动程序提供,驱动可以知道自身的数据是否可用。支持阻塞操作的设备驱动通常会实现一组自身的等待队列,如读/写等待队列用于支持上层(用户层)所需的block或non-block操作。设备的文件的资源如果可用(可读或者可写)则会通知进程,反之则会让进程睡眠,等到数据到来可用的时候,再唤醒进程。
这些设备的文件描述符被放在一个数组中,然后select调用的时候遍历这个数组,如果对于的文件描述符可读则会返回改文件描述符。当遍历结束之后,如果仍然没有一个可用设备文件描述符,select让用户进程则会睡眠,直到等待资源可用的时候在唤醒,遍历之前那个监视的数组。每次遍历都是线性的。
fd_r_list, fd_w_list, fd_e_list = select.select(rlist, wlist, xlist, [timeout])
参数: 可接受四个参数(前三个必须)
rlist: wait until ready for reading
wlist: wait until ready for writing
xlist: wait for an “exceptional condition”
timeout: 超时时间
返回值:三个列表
select方法用来监视文件描述符(当文件描述符条件不满足时,select会阻塞),当某个文件描述符状态改变后,会返回三个列表
1. 当参数1 序列中的fd满足“可读”条件时,则获取发生变化的fd并添加到fd_r_list中
2. 当参数2 序列中含有fd时,则将该序列中所有的fd添加到 fd_w_list中
3. 当参数3 序列中的fd发生错误时,则将该发生错误的fd添加到 fd_e_list中
4. 当超时时间为空,则select会一直阻塞,直到监听的句柄发生变化
当超时时间 = n(正整数)时,那么如果监听的句柄均无任何变化,则select会阻塞n秒,之后返回三个空列表,如果监听的句柄有变化,则直接执行。
为了较为及时地获得fd的状态,我们需要不停的调用select, 这就意味着:
1. 当文件描述符过多时,文件描述符在用户空间与内核空间进行copy会很费时
2. 当文件描述符过多时,内核对文件描述符的遍历也很浪费时间
3. select最大仅仅支持1024个文件描述符
在asyncore.py的最开始有一个全局字典socket_map对应fd和一个asyncore.dispatcher类,
当你继承一个dispatcher类的时候总会调用函数在全局字典socket_map中创建一个映射,当然在关闭的时候也会从全局字典中删除
最后是一个loop死循环,可以很容易想到是使用了系统IO多路复用接口select.select对全局socket_map进行了操作。根据返回的事件进行操作(3个事件读、写、错误)
流程图示
一个简单的聊天室CASE
server
from asyncore import dispatcher
from asynchat import async_chat
import socket, asyncore
PORT = 5005
NAME = 'TestChat'
class EndSession(Exception): pass
class CommandHandler:
def unknown(self, session, cmd):
session.push(('Unknown command: %s\r\n' % cmd).encode())
def handle(self, session, line):
if not line.strip(): return
parts = line.split(' ', 1)
cmd = parts[0]
try: line = parts[1].strip()
except IndexError: line = ''
meth = getattr(self, 'do_' + cmd, None)
try:
meth(session, line)
except TypeError:
self.unknown(session, cmd)
class Room(CommandHandler):
def __init__(self, server):
self.server = server
self.sessions = []
def add(self, session):
self.sessions.append(session)
def remove(self, session):
self.sessions.remove(session)
def broadcast(self, line):
for session in self.sessions:
session.push(line.encode())
def do_logout(self, session, line):
raise EndSession
class LoginRoom(Room):
def add(self, session):
Room.add(self, session)
self.broadcast('Welcome to %s\r\n' % self.server.name)
def unknown(self, session, cmd):
session.push(('Please log in\nUse "login <nick>"\r\n').encode())
def do_login(self, session, line):
name = line.strip()
if not name:
session.push(('Please enter a name\r\n').encode())
elif name in self.server.users:
session.push(('The name "%s" is taken.\r\n' % name).encode())
session.push(('Please try again.\r\n').encode())
else:
session.name = name
session.enter(self.server.main_room)
class ChatRoom(Room):
def add(self, session):
self.broadcast(session.name + ' has entered the room.\r\n')
self.server.users[session.name] = session
Room.add(self, session)
def remove(self, session):
Room.remove(self, session)
self.broadcast(session.name + ' has left the room.\r\n')
def do_say(self, session, line):
self.broadcast(session.name + ': ' + line + '\r\n')
def do_look(self, session, line):
session.push(('The following are in this room:\r\n').encode())
for other in self.sessions:
session.push((other.name + '\r\n').encode())
def do_who(self, session, line):
session.push(('The following are in this room:\r\n').encode())
for name in self.server.users:
session.push(((name + '\r\n').encode()).encode())
class LogoutRoom(Room):
def add(self, session):
try: del self.server.users[session.name]
except KeyError: pass
class ChatSession(async_chat):
def __init__(self, server, sock):
async_chat.__init__(self, sock)
self.server = server
self.set_terminator(('\r\n').encode())
self.data = []
self.name = None
self.enter(LoginRoom(server))
def enter(self, room):
try: cur = self.room
except AttributeError: pass
else: cur.remove(self)
self.room = room
room.add(self)
def collect_incoming_data(self, data):
self.data.append(data.decode())
def found_terminator(self):
line = ''.join(self.data)
self.data = []
try: self.room.handle(self, line)
except EndSession:
self.handle_close()
def handle_close(self):
async_chat.handle_close(self)
self.enter(LogoutRoom(self.server))
class ChatServer(dispatcher):
def __init__(self, port, name):
dispatcher.__init__(self)
self.create_socket(socket.AF_INET, socket.SOCK_STREAM)
self.set_reuse_addr()
self.bind(('', port))
self.listen(5)
self.name = name
self.users = {}
self.main_room = ChatRoom(self)
def handle_accept(self):
conn, addr = self.accept()
ChatSession(self, conn)
if __name__ == '__main__':
s = ChatServer(PORT, NAME)
try: asyncore.loop()
except KeyboardInterrupt: print()
client
from asyncore import dispatcher
from asynchat import async_chat
import socket, asyncore
PORT = 5005
class EndSession(Exception): pass
class CommandHandler:
def unknown(self, session, cmd):
session.push(('Unknown command: %s\r\n' % cmd).encode())
def handle(self, session, line):
if not line.strip(): return
parts = line.split(' ', 1)
cmd = parts[0]
try: line = parts[1].strip()
except IndexError: line = ''
meth = getattr(self, 'do_' + cmd, None)
try:
meth(session, line)
except TypeError:
self.unknown(session, cmd)
class client(async_chat):
def __init__(self, server, port):
async_chat.__init__(self)
self.create_socket(socket.AF_INET, socket.SOCK_STREAM)
self.set_terminator(('\r\n').encode())
self.data = []
self.name = None
self.connect((server, port))
def collect_incoming_data(self, data):
self.data.append(data.decode())
def found_terminator(self):
line = ''.join(self.data)
self.data = []
try: self.room.handle(self, line)
except EndSession:
self.handle_close()
def handle_close(self):
async_chat.handle_close(self)
def handle_connect(self):
pass
def do_something(self, line):
pass
if __name__ == '__main__':
s = client(PORT, IP)
try: asyncore.loop()
except KeyboardInterrupt: print()
参考博客:https://www.zoulei.net/2016/06/29/asyncore_note/index.html