IO多路复用
同步io和异步io,阻塞io和非阻塞io分别是什么,有什么样的区别?
io模式
对于一次io 访问(以read为例),数据会先拷贝到操作系统内核的缓冲区,然后才会从操作系统内核的缓冲区拷贝到应用程序的地址空间。所以说,当一个read操作发生时,它会经历两个阶段:
1.等待数据准备
2.将数据从内核拷贝到用户的进程中
在linux中系统产生了下面五种网络模式的方案:
1.阻塞i/o 2.非阻塞i/o 3.i/o多路复用 4.信号驱动i/o(实际应用中不多) 5.异步i/o
阻塞i/o
在linux中,默认情况下所有的socket都是阻塞的(blocking),一个经典的读操作流程大概是这个样的:
当用户进程调用recvfrom是,kernel就开始了第一阶段的io,准备数据,这个过程需要等待,也就是说数据被拷贝到操作系统内核的缓冲区中是需要一个过程的,而在用户进程那,整个进程会被阻塞。当kernel一直等待数据准备好了,就会将数据从kernel中拷贝到用户内存,然后kernel返回结果,用户进程才解除阻塞状态,重新运行起来
即它的特点就是:在io执行的两个阶段都被block了
非阻塞I/O
linux下,可以通过设置socket使其变成了非阻塞,对一个非阻塞的socket执行读操作流程如下:
在用户进程发出read操作的时候,如果kernel中的数据还没有准备好的话,它不会阻塞用户进程,而是立刻返回一个error,从用户进程角度来说,它发起一个read操作之后,并不需要等待,而是马上得到了一个结果。用户进程判断结果如果是一个error,就知道了数据没有准备好,于是再次发送read操作,一旦kernel中数据准备好了,并且又再次收到了用户进程的system call,那么它马上就将数据拷贝到用户内存,然后返回
即它的特点就是:用户进程需要不断的主动的询问kernel数据准备好了没有
相当于我们第一次调用recv时,如果没有数据,我们可以去干其他的事情,从这个层面上来看,它比阻塞io要好,但仅仅只是非阻塞,下面我们看另外的模式:
I/O多路复用
io多路复用就是我们所说的select,poll,epoll,有些地方也称这种io方式为event driven io(事件驱动),select/epoll的好处就是在于单个process就可以同时处理多个网络连接的io。它的基本原理就是select ,poll,epoll这个function会不断的轮询所负责的所有的socket,当socket有数据的时候,就通知用户进程。
在单线程下,如果我们采取阻塞io,一收数据就卡住,我们就没有办法实现在一个单线程下操作多个socket连接,即单线程下阻塞io模式下是没有办法实现操作多路io
如果变成单线程下,非阻塞模式,建立100个连接,我们不知道哪个socket会发送数据,我们循环收取,100连接可能有5个发送了数据,95个没有发数据,但是没有关系,没有数据就直接走下一个循环不会卡住,最终把这5个数据收到,这种情况下我们已经实现了操作多个socket,在用户看来就已经是多并发了。不过有一个小问题,在从内核态到用户态发送的时候还是会卡住,等待一个copy时间,如果数据量小的话没有什么大问题,如果数据量大的话就会卡很久。虽然多并发,但是还是会卡
对于I/O多路复用:当用户进程调用了select,那么整个进程就会被block,同时,kernel会“监视”所有select负责所有的socket,当有一个socket中数据准备好,selct就会返回,这个时候用户进程再调用read,将数据从kernel拷贝到用户进程
即它的特点就是:通过一种机制一个进程能同时等待多个文件描述符(可以理解为socket连接),而这些文件描述符其中的任意一个进入读就绪状态,select()函数局可以返回
和阻塞io的区别就是:一开始是一个socket调用recv卡住了,而io多路复用是,一开始可能有很多个socket调用select()
异步I/O
用户进程发起read操作之后,立刻就可以去干其他的事情,另一方面,从kernel角度,他们收到一个异步的read操作之后,首先会立刻返回(用户就可以在这个时候去操作其他的东西),所以不会对用户进程产生任何的block。然后kernel会等待数据准备完成,再将数据拷贝到用户内存,当这一切完成之后,kernel会给用户进程发送一个signal,告诉它read操作完成了
就相当于在最后从内核态拷贝到用户态那个环节也不会卡住
下面我们就详细了解一下多路复用的select,poll,epoll:
select:
select目前支持所有的平台,其良好的跨平台是是他的一个优点,缺点就是单个进程能够监视的文件描述符的数量是存在最大限制的,在linux上一般为1024,不过可以通过修改宏定义甚至重新编译内核的方式提升,
另外,select()所维护的存储大量的文件描述符的数据结构,随着文件miaoshu描述符的数量增大,其复制的开销也就不断的增大。同时,由于网络响应时间的延迟使得大量tcp连接处于非活跃状态,但调用select()会对所有的socket进行一次线性扫描,所以在一定程度上造成了浪费
poll:
和select没有多大的区别,但是poll没有最大文件描述符数量的限制,缺点也差不多,
epoll:
epoll是由内核直接支持的实现方法,几乎具备了之前的一切优点。windows不支持epoll,linux支持
简单的说, 如果有100socket交给内核去检测,只不过当100里面有一个链接活跃了,这个内核会告诉用户哪一个链接有数据,所以用户直接取到这个链接去读数据就行了,这就是epoll最主要的有点。如果我们有6万个链接里面有两个活跃,只要循环去这两个就可以了。
epoll也可以同时支持水平触发和边缘触发(水平触发就是说100个连接中有两个有数据,活跃了,内核返回给用户的程序,用户去取数据,但是这个时候用户有其他的事情,没有来得及去处理,没有去取数据,那数据还在内核态,除非用户主动的去取数据,这个数据才会从内核态拷贝到用户态。下一次可以继续监测再通知这个有数据的连接里;边缘触发就是说如果数据来了,告诉用户数据准备好了可以来取了,如果用户没有去取数据,那就数据就一直存储在内核中,但是下次不会再次通知,所以数据对于用户来说就没有了)
epoll没有最大socket数的限制,依然是io多路复用,不是异步io
上面讲了这么多,我们用代码来看看这个selec,poll,epoll,看看实现出来是什么样子的,
select:
python的select()方法直接调用操作系统的io调用,它监控sockets,open files,and pipes何时变成readable,writeable,或者通信错误,select()使得同时监控多个连接变的简单,并且这个比写一个长循环来等待和监控客户端连接更加的高效,因为select直接通过操作系统提供的c的网络接口进行操作的。而不是python解释器
下面我们就用echo server例子来了解select是如何通过单进程实现同时处理多个非阻塞的socket连接的
import select import socket import queue server = socket.socket() server.bind(('localhost',9999)) server.listen(1000) server.setblocking(False)#设置成非阻塞 inputs = [server,] outputs = [] msg_dic = {} while True: readable,writeable,exceptional = select.select(inputs,outputs,inputs) print(readable,writeable,exceptional) for r in readable: if r is server:#代表来了一个新连接 conn,addr = server.accept() print("来了一个新连接",addr) #是因为这个新建立的连接还没有发送数据过来,现在接受数据的话程序就会报错,所以要想实现这个客户端发数据来时,server端能够知道,就需要让 #select再检测这个conn inputs.append(conn) msg_dic[conn] = queue.Queue()#初始化一个队列,后面存要返回给这个客户端的数据 else: data = r.recv(1024) print("recv: ",data) msg_dic[r].put(data) #在下一次循环的时候发送数据 outputs.append(r)#放入返回的连接队列中 # r.send(data) # print("send done....") for w in writeable:#要返回给客户端的连接列表 data_to_client = msg_dic[w].get() w.send(data_to_client)#返回给客户端原数据 outputs.remove(w)#确保下次循环的时候writeable不返回已经处理完的连接 for e in exceptional: if e in outputs: outputs.remove(e) inputs.remove(e) del msg_dic[e]
首先我们要用到的是server.setblocking(False)把他设置成非阻塞模式,
然后我们设置了inputs,outputs两个列表,我们想要内核帮我们监测100个连接,我们要传一个列表,有多少个连接就把他都放在inputs中,
使用select.select(),里面有3个参数,rlist,wlist,xlist,分别是你想要监测的连接,想要输出的数据,100个中如果有5个断开了操作系统就会把这些放到xlist
如果运行出现了这个错误的话,这个就是说在inputs这个列表中没有放入你要监测的连接,这里我们自己server本身就是一个监测对象,所以可以写入inputs,监测自己,然后运行就没有问题了:inputs[server,]
select.select()返回三个数据:readable,writeable,exceptional
如果我们在select.select()直接server.accept(),这样连接客户端之后server端就直接结束运行,所以我们要在前面用for循环,循环readable
在接受数据的时候我们可能会遇到这样的错误:(直接用recv方法接受数据,就是说在accept()之后直接用conn.recv(1024)接受数据)
这个是由于连接过来之后客户端没有发送数据,server端没有收到数据但是不阻塞,所以就会报错,那么我们怎么知道他发数据的时候知道,那我们就可以把这个连接加入到inputs列表,这样在下一次检测的时候,如果活动了,说明有数据过来了:inputs.append(conn),这个时候相当于inputs这个列表里有server,conn两个连接,如果是返回活动的是server说明是又来了一个新连接,加入到inputs中,如果返回活动的是conn,那我们就可以直接接收数据,所以要用if判断。
在接收数据和发送数据的时候,我们接受数据可以放在一个队列字典中,等一会发送给客户端。所以我们在建立连接 的时候就可以给这个连接建立一个queue,然后在接受的数据之后可以先存到这个连接的字典中,那我们什么时候发送呢?
我们是在下一次循环发送数据,怎么实现呢,需要用我们刚才的outputs列表,你往outputs中放什么数据,你就会输出什么数据。所以就用到上面代码中的循环等,在用完之后要删除。
当然上述代码还不是很完善,当我们在关闭一个客户端运行的时候,我们server端也就停止运行了:
服务器端:
1 import queue 2 import select 3 import socket 4 5 server = socket.socket() 6 server.bind(("localhost",9999)) 7 server.setblocking(False) 8 server.listen(1000) 9 10 inputs = [server,] 11 outputs = [] 12 msg_dic = {} 13 14 while True: 15 print("waiting for next event....") 16 readable,writeable,exceptional = select.select(inputs,outputs,inputs) 17 #print(readable,writeable,exceptional) 18 for r in readable:#每个r就是一个新的连接 19 if r is server:#这里判断如果是server就表明有新的连接进入,之前我们就已经把我们自己server放在了inputs中 20 conn,addr = r.accept() 21 conn.setblocking(False) 22 inputs.append(conn)#为了不阻塞整个程序,我们不会立刻在这里开始接收客户端发来的数据, 把它放到inputs里, 下一次loop时,这个新连接 23 #就会被交给select去监听,如果这个连接的客户端发来了数据 ,那这个连接的fd在server端就会变成就续的,select就会把这个连接返回,返回到 24 #readable 列表里,然后你就可以loop readable列表,取出这个连接,开始接收数据了, 下面就是这么干的 25 msg_dic[conn] = queue.Queue()#接受到的客户端的数据之后,不立刻返回,而是存在消息队列中,以后发送 26 else:#如果r不是server的话,那就说明没有新的连接进入,我们需要接受客户端传过来的数据, 27 data = r.recv(1024) 28 if data: 29 print("收到来自[%s]发过来的数据:%s" %(r.getpeername()[0],data)) 30 msg_dic[r].put(data) 31 if r not in outputs: 32 outputs.append(r)#为了不影响处理与其他客户端的连接,这里不立刻返回数据 33 else:#如果收不到数据,代表客户端断开 34 print("客户端断开",r) 35 #清理已经断开的连接 36 if r in outputs: 37 outputs.remove(r) 38 inputs.remove(r) 39 del msg_dic[r] 40 for w in writeable: 41 try: 42 next_msg = msg_dic[w].get_nowait() 43 44 except queue.Empty: 45 print("client [%s]" % w.getpeername()[0], "queue is empty..") 46 outputs.remove(w) 47 48 else: 49 print("sending msg to [%s]" % w.getpeername()[0], next_msg) 50 w.send(next_msg.upper()) 51 for e in exceptional: 52 print("handling exception for ",e.getpeername()) 53 inputs.remove(e) 54 if e in outputs: 55 outputs.remove(e) 56 e.close() 57 58 del msg_dic[e]
客户端:
1 import socket 2 3 messages = [ b'This is the message. ', 4 b'It will be sent ', 5 b'in parts.', 6 ] 7 server_address = ('localhost', 9999) 8 9 socks = [ socket.socket(socket.AF_INET, socket.SOCK_STREAM), 10 socket.socket(socket.AF_INET, socket.SOCK_STREAM), 11 ] 12 for s in socks: 13 s.connect(server_address) 14 15 for message in messages: 16 17 # Send messages on both sockets 18 for s in socks: 19 print('%s: sending "%s"' % (s.getsockname(), message) ) 20 s.send(message) 21 22 # Read responses on both sockets 23 for s in socks: 24 data = s.recv(1024) 25 print( '%s: received "%s"' % (s.getsockname(), data) ) 26 if not data: 27 print('closing socket', s.getsockname() )
这样可以在客户端中多并发的执行多个socket
selector模块
直接上代码:
1 import selectors 2 import socket 3 4 sel = selectors.DefaultSelector() 5 6 7 def accept(sock, mask): 8 conn, addr = sock.accept() 9 print('accepted', conn, 'from', addr) 10 conn.setblocking(False) 11 sel.register(conn, selectors.EVENT_READ, read) 12 13 14 def read(conn, mask): 15 data = conn.recv(1000) 16 if data: 17 print('echoing', repr(data), 'to', conn) 18 conn.send(data) 19 else: 20 print('closing', conn) 21 sel.unregister(conn) 22 conn.close() 23 24 25 sock = socket.socket() 26 sock.bind(('localhost', 9999)) 27 sock.listen(100) 28 sock.setblocking(False) 29 sel.register(sock, selectors.EVENT_READ, accept) 30 31 while True: 32 events = sel.select()#默认是阻塞,有活动连接就返回活动的连接列表 33 for key, mask in events: 34 callback = key.data#相当于调用accept 35 callback(key.fileobj, mask)#key.fileobj就是文件句柄,相当于上面程序的r
selector.DefaultSelector()和select.select()差不多,生成一个select对象,然后下面用sel.register(sock,selectors.Event_READ,accept)注册一个事件,让它监听,只要来一个新的连接就调用accept函数
events = sel.select()这句话也有可能调用的是epoll,看你系统支持什么
然后在结合客户端代码,就可以了,速度很快:
初次学习,做做笔记,用于加深印象,如果有错误的话,希望大家积极纠正,在此谢过大家啦