一、事件驱动模型
通常我们写服务器处理模型程序时候,有以下几个模型:
- 每收到一个请求,创建一个新的进程,来处理该请求
- 每收到一个请求,创建一个新的线程,来处理该请求
- 每收到一个请求,放入一个事件列表,让主进程通过非阻塞I/O方式来处理请求(事件驱动)
以上3点优缺点:
- 创建进程的开销比较大,导致服务器性能变差,但实现比较简单
- 涉及到线程同步问题,可能会死锁
- 逻辑较为复杂,但是大多数网络服务器都采用这种方法
那么什么是事件驱动模型呢?举一个简单的例子:
当我们使用鼠标时候,那么电脑如何获取鼠标点击呢?
(1)创建一个线程,该线程一直循环检测是否有鼠标点击。
缺点:浪费CPU资源、容易堵塞(点1下还没处理完再点击无用)、不能同时检测鼠标和键盘(一次只能干一个事情)
(2)事件驱动模型:根据不同的事件做出不同的反应,很多UI平台都会提供onClick()事件。事件驱动模型的思路:
1.有一个事件队列
2.鼠标按下时,向队列中加入一个点击事件
3.有一个循环程序,不断地从队列取出事件,根据不同的事件,调用不同的函数
4.事件一般都各自保存各自的处理函数指针,这样,每个消息都有独立的处理函数
5.当处理线程处理完当前任务,要执行一个回调函数,告诉你该任务已经执行完毕
这就是事件驱动模型。
二、异步I/O
想要说异步I/O,我们要先谈谈linux系统下的4种网络模式方案。(阻塞I/O,非阻塞I/O,I/O多路复用,异步I/O)再说这个之前我们要先理解操作系统如何接收数据》》》数据通过socket发过来,先由操作系统存在内核空间,在从内核空间发送到用户空间(内存)。下面说一下4种模型:
阻塞I/O:在linux中,默认情况下所有的socket都是阻塞的,在等待socket发数据时候,是阻塞的。当接收的数据被
拷贝到内核缓冲区时候,也会阻塞。
非阻塞I/O:在接收数据时候不会阻塞,一旦数据没准备好,返回一个error(一旦发现是error就知道数据没准备好)
可以再次接收数据或者去干别的,内核数据拷贝到用户空间还是阻塞的。
I/O多路复用(三种模式select,poll,epoll):这三种模式就是在不断地检查每个socket是否有数据发送过来
单一进程单一线程,当用户调用select,那么整个进程就会被阻塞,同时内核会监听select负责的所有socket,
当任何一个准备好,select就会返回数据,然后把数据从内核拷贝到用户空间。
异步I/O:用户发起请求,就不用管了,可以干别的事。不会阻塞进程。内核去处理数据,当结束后发送一个signal给用户
告诉用户完成了。
I/O多路复用中:
select:每次都要循环n个socket连接,浪费资源,只能监控n个连接
poll:取消了最多连接个数n,可以监控更多的连接
epoll:监控socket,会返回哪一个有数据,数据还在内核中,用户需要自己调用recv等,这样就不用全都循环一遍了
介绍一下I/O多路复用中的select模块:
select服务器:
#用select模拟socket server
#需要在非阻塞模式下进行
import select,socket,queue
server=socket.socket()
server.bind(('localhost',9000))
server.listen(1000) #监听1000个
#设置非阻塞模式
server.setblocking(False) #设置非阻塞模式
inputs=[server,] #监听的连接放在这里
outputs=[]
while True:
readable,writeable,exceptional=select.select(inputs,outputs,inputs ) #第一个inputs是检测哪些链接,第二个inputs返回有问题的链接
print(readable,writeable,exceptional)
for r in readable:
if r is server: #代表进来一个新链接
conn,addr=server.accept() #没有连接就报错
print('来了一个新链接:',addr)
#建立连接完成,开始收数据,需要让内核通知你要发数据了,才能收
inputs.append(conn) #要想实现客户端发数据来时,server能知道,就需要让select再监测这个conn
#这样一来inputs中就有server和 conn,如果返回server说明又有新链接进来,如果返回conn说明可以接收数据了
else:
data=r.recv(1024)
print('recv:',data)
r.send(data)
print('send done ....')
select 客户端:
import socket
HOST='localhost'
PORT=9000
s=socket.socket(socket.AF_INET,socket.SOCK_STREAM)
s.connect((HOST,PORT))
while True:
msg=bytes(input('>>:'),encoding= 'utf-8')
s.sendall(msg)
data=s.recv(1024)
print('Received',repr(data)) #repr格式化输出
s.close()
看一下结果:
我们开启两个客户端,分别给服务器发送消息,可以看见服务器连接了两个 客户端,也接收到了客户端数据。这就是I/O多路复用的一种模式,监听socket是否有消息发过来,并把数据存在用户空间。