一.事件驱动模型
1.什么是事件驱动模型:本身是一种编程范式,这里程序的执行是由外部事件来决定的。它的特点是包含一个事件循环,当外部事件发生时使用回调机制来触发相应的处理。常见的编程范式(单线程)同步以及多线程编程
2.事件驱动模型流程:开始-->初始化-->等待
3.事件驱动模型的原理:目前大部分的UI编程都是事件驱动模型,如很多UI平台都会提供onCick()事件,事件驱动模型大体思路如下:
(1)有一个事件(消息)队列
(2)鼠标按下时,往这个队列中增加一个点击事件(消息)
(3)有个循环,不断从队列中取出事件,根据不同的事件,调用不同的函数,如onClick(),onKeyDown()等
(4)事件(消息)一般都是各自保存各自的处理函数指针,这样,每个消息都有独立的处理函数
二.IO模型前了解的概念
1.用户空间与内核空间:
(1)现在操作系统都采用虚拟存储器,那么对32位操作系统而言,它的寻址空间(虚拟存储空间)为4G(2的32次方)。操作系统的核心是内核,独立于普通的应用程序,可以访问受保护的内存空间,也有访问底层硬件设备的所有权限。为了保证用户进程不能直接操作内核,保证内核的安全,操作系统将虚拟空间划分为俩部分,一部分为内核空间,一部分用户空间,通过CPU的指令集(CPU执行的代码)上的状态位来决定什么时候是用户态,什么时候是内核态
(2)内核空间:针对linux操作系统而言,将最高的1G字节,供内核使用,称为内核空间
(3)用户空间:针对linux操作系统而言,将较低的3G字节,供各个进程使用,称为用户空间
2.进程切换(非常耗资源)
(1)为了控制进程的执行,内核必须有能力挂起正在CPU上运行的进程,并恢复以前挂起的某个进程的执行。这种行为被称为进程切换,这种切换操作是由操作系统来完成的。因此可以说,任何进程都是在操作系统内核的支持下运行的,是与内核紧密相关的
(2)从一个进程的运行转到另一个进程上运行,这个过程中经过以下变化:
1)保存处理机上下文,包括程序计数器和其他寄存器。
2)更新PCB信息
3)把进程的PCB移入相应的队列,如果就绪,在某事件阻塞等待队列
4)选择另一个进程执行,并更新其PCB
5)更新内存管理的数据结构。
6)恢复处理机上下文。
3.进程的阻塞(当进程进入阻塞状态,是不占用CPU资源的)
正在执行的进程,由于期待的某些事件未发生,如请求系统资源失败,等待某种操作的完成,新数据尚未到达或无新工作做等,则由系统自动执行阻塞原语,使自己由运动状态变为阻塞状态。可见,进程的阻塞是进程自身的一种主动行为,也因此只有处于运动态的进程(获得CPU),才能将其转为阻塞状态。
4.文件描述符:socket就是文件描述符
文件描述符是计算机中的一个术语,是一个用于表述指向文件的引用的抽象化概念。文件描述符在形式上是一个非负整数。实际上它是一个索引值,指向内核为每一个进程所维护的该进程打开文件的记录表。当程序打开一个现有文件或者创建一个新文件时,内核向进程返回一个文件描述符。在程序设计中,一些涉及底层的程序缩写往往会围绕着文件描述符展开。但文件描述符这一概念只适用于UNIX,linux这样的操作系统
5.缓存I/O
(1)缓存I/O大多数文件系统的默认I/O操作都是缓存I/O。在linux的缓存I/O机制中,操作系统会将I/O的数据缓存在文件系统的页缓存中,也就是说,数据会先被拷贝到操作系统内核的缓冲区中,然后才会从操作系统内核的缓冲区拷贝到应用程序的地址空间。用户空间没法直接访问内核的空间的,内核态到用户态的数据拷贝。
(2)缓存I/O的缺点:数据在传输过程中需要在应用程序地址空间和内核进行多次数据拷贝操作,这些数据拷贝操作所带来的CPU以及内存开销是非常大的。
三.四种I/O模型
1.blocking IO(阻塞I/O):从开始到结束全程堵塞:
(1)在linux中,默认情况下所有的socket都是blocking
(2)当用户进程调用了recvfrom这个系统调用,kerne就开始了IO的第一个阶段:准备数据。对于network io来说,很多时候数据在一开始还没有到达,这个时候kernel(内核)就要等待足够的数据到来。而在用户进程这边,整个进程会被阻塞。当kernel一直等到数据准备好了,它就会将数据从kernel中拷贝到用户内存,然后kernel返回结果,用户进程才解除block的状态,重新运行起来。所以,blocking IO的特点就是在IO执行的俩个阶段都被block了(阻塞IO只发了一次系统调用)
(3)结合代码:
客户端代码:
import socket sk=socket.socket() sk.connect(("127.0.0.1",8080)) #第二步:客户端connect先发消息到服务端 while 1: data=sk.recv(1024) #第五步:客户端接收服务端发来的消息(如果服务端不发过来消息,客户端会阻塞住) print(data.decode("utf8")) sk.send(b"hello server")
服务端代码:
import socket sk=socket.socket() sk.bind(("127.0.0.1",8080)) sk.listen(5) while 1: #第一步:服务端程序启动accept()跟操作系统打交道,向操作系统要数据,发一条命令recvfrom是系统调用,操作系统内核在等待数据这个过程是阻塞状态 conn,addr=sk.accept() #第三步:当客户端启动执行connect方法链接上服务端,服务端内核区有数据了,它就会将数据从内核中拷贝到用户内存接收消息拿到一个值,程序继续进行 while 1: conn.send("hello client".encode("utf8")) #第四步:给客户端发消息 data=conn.recv(1024) print(data.decode("utf8"))
2.non-blocking IO(非阻塞I/O)
(1)在linux中,设置socket使其变为non-blocking
(2)当用户进程发出read操作时,如果kernel中数据还没有准备好,那么它并不会block用户进程,而是立刻返回error。从用户进程角度讲,它发起一个read操作后,并不需要等待,而是马上就得到了一个结果。用户进程判断结果是一个error时,它就知道数据还没有准备好,于是它可以再次发送read操作。一旦kernel中的数据准备好了,并且又再次收到了用户进程的system call,那么它马上就将数据拷贝到了用户内存,然后返回。
(3)缺点:
1)用户进程需要不断的主动询问kerrel数据好了没有
(4)结合代码
客户端:
import time import socket sk = socket.socket(socket.AF_INET,socket.SOCK_STREAM) while True: sk.connect(('127.0.0.1',8080)) #第三步:链接服务端 print("hello") sk.sendall(bytes("hello","utf8")) #第五步:给服务端发送一条数据 time.sleep(2) break
服务端:
import time import socket sk = socket.socket(socket.AF_INET,socket.SOCK_STREAM) sk.bind(('127.0.0.1',8080)) sk.listen(5) sk.setblocking(False) #setblocking设置成非阻塞IO print ('等待客户端链接 .......') while True: try: #进程主动轮询 connection,address = sk.accept() #第一步:设置成非阻塞IO等待链接的时候不会卡住(会报错) print("+++",address) #第四步:打印链接到服务端的客户端IP和端口 client_messge = connection.recv(1024) #第六步:接收客户端数据 print(str(client_messge,'utf8')) #第七步:打印接收数据 connection.close() #第八步:关闭继续循环 except Exception as e: #第二步:捕捉到错误继续往下执行代码 print (e) time.sleep(4)
输出打印:
等待客户端链接 .......
[WinError 10035] 无法立即完成一个非阻止性套接字操作。
[WinError 10035] 无法立即完成一个非阻止性套接字操作。
+++ ('127.0.0.1', 64251)
hello
[WinError 10035] 无法立即完成一个非阻止性套接字操作。
....
....
....
3.I/O多路复用(同步I/O)
(1)什么是I/O多路复用:
单个process可以同时处理多个网络连接的IO,它的基本原理就是select/peoll这个function会不断的轮询锁负责的所有socket,当某个socket有数据到达了,就通知用户进程。当用户进程调用了select,那么整个进程会被block,而同时,kernel会监听所有select负责的socket,当任何一个socket中的数据准备好了,select就会返回。这个时候用户进程在盗用read操作,将数据从kernel拷贝到用户进程。
(2)I/O多路复用的触发方式:在linux的IO多路复用中有水平触发,边缘触发俩种模式
1)水平触发:如果文件描述符已经就绪可以非阻塞的执行IO操作了,此时会触发通知,允许在任意时刻重复检测IO的状态,没有必要每次描述符就绪后尽可能多的执行IO.select,poll就属于水平触发
2)边缘触发:如果文件描述符自上次状态后有新的IO活动到来,此时会触发通知,在收到一个IO事件通知后要尽可能多的执行IO操作,因为如果在一次通知中没有执行完IO那么就需要等到下一次新的IO活动的到来才能获取就绪的描述符,信号驱动式IO就是属于边缘触发
(3)从电子的角度来解释水平触发和:
1)水平触发:也就是只有高电平(1)或低电平(0)时才触发通知,只要在这俩种状态就能得到通知,只要有数据可读(描述符就绪)那么水平触发的epoll就立即返回。
2)边缘触发:只有电平发生变化(高电平到低电平,或者低电平到高电平)的时候才触发通知,即使有数据可读,但没有新的IO活动到来,epoll也不会立即返回
(4)IO多路复用的优点:可同时监听多个链接
(5)IO多路复用的的实现方法:select、poll、epoll
1)select实现方法:
缺点:每次调用都要将所有的文件描述符(fd)拷贝的内核空间,导致效率下降,遍历所有的文件描述符(fd)查看是否有数据访问,最大链接数限额1024
现实生活举例:班里三十个同学在考试,谁先做完交卷都要通过按钮来活动,一旦谁按了按钮老师桌子上的灯就会变红,一旦灯变红,老师(select)就知道有人交卷, 但并不知道是谁交的,所以老师必须轮询的一个一个同学问,就可以以这种效率极低的方式找到要交卷的学生,把卷子收上来
通过select单线程实现并发
服务端:
import socket import select sk=socket.socket() sk.bind(("127.0.0.1",8080)) sk.listen(5) inputs=[sk,] #sk是socket对象(本身是文件描述符,对应一张表存在的),可以监听多个socket对象 while True: #第一步:启动程序监听会有阻塞的状态,sk处于没有变化的状态 r,w,e=select.select(inputs,[],[],5) #第三步:当客户端链接后,select一旦监听到sk,把sk赋值给r,r就是sk(除非有新的用户来否则sk不会发生变化)当前inputs是[sk,] 第十步:当前r就等于列表中就有sk和conn:当前inputs是[sk,conn] for obj in r: #第四步:循环r,当前r就等于[sk,] 第十一步:循环r,当前r就等于[sk,conn] #对进来的r进行判断(遍历进来的是否是sk还是conn) if obj==sk: #第五步:判断如果是sk就是接收新的用户 conn,add=obj.accept() #第六步:obj.accept接收此时链接服务端的socket对象conn print(conn) inputs.append(conn) #第八步:把对方的socket对象放到inputs=[]里的列表里 else: #第十二步:判断如果是conn就可以收发消息 data_byte=obj.recv(1024) print(str(data_byte,'utf8')) inp=input('回答%s号客户>>>'%inputs.index(obj)) obj.sendall(bytes(inp,'utf8')) print('>>',r) #第九步:打印
客户端1:
import socket sk = socket.socket() sk.connect(('127.0.0.1', 8080)) #第二步:客户端链接服务端 while True: inp = input(">>>>") sk.sendall(bytes(inp, "utf8")) #第七步:客户端发信息给服务端 data = sk.recv(1024) #接收服务端返回的信息 print(str(data, 'utf8')) #打印服务端返回来的信息
客户端2:
import socket sk = socket.socket() sk.connect(('127.0.0.1', 8080)) #第二步:客户端链接服务端 while True: inp = input(">>>>") sk.sendall(bytes(inp, "utf8")) #第七步:客户端发信息给服务端 data = sk.recv(1024) #接收服务端返回的信息 print(str(data, 'utf8')) #打印服务端返回来的信息
服务端打印信息:每5秒打印[],当有客户端链接打印客户端链接的信息
>> []
>> [<socket.socket fd=348, family=AddressFamily.AF_INET, type=SocketKind.SOCK_STREAM, proto=0, laddr=('127.0.0.1', 8080)>]
>> [<socket.socket fd=348, family=AddressFamily.AF_INET, type=SocketKind.SOCK_STREAM, proto=0, laddr=('127.0.0.1', 8080)>]
>> []
我是客户端1
回答1号客户
>>>给客户端1返回
>> [<socket.socket fd=368, family=AddressFamily.AF_INET, type=SocketKind.SOCK_STREAM, proto=0, laddr=('127.0.0.1', 8080), raddr=('127.0.0.1', 59695)>]
我是客户端2
回答2号客户
>>>给客户端2返回
>> [<socket.socket fd=376, family=AddressFamily.AF_INET, type=SocketKind.SOCK_STREAM, proto=0, laddr=('127.0.0.1', 8080), raddr=('127.0.0.1', 59698)>]
>> []
>> []
客户端1打印信息:
>>>>我是客户端1
给客户端1返回
>>>>
客户端2打印信息:
>>>>我是客户端2
给客户端2返回
>>>>
2)poll实现方法:它就是select和epoll的过渡阶段,它没有最大链接数的限额
3)epoll实现方法:
第一个函数是创建一个epoll句柄,将所有的描述符(fd)拷贝到内核空间,但只拷贝一次。回调函数,某一个函数或某一个动作成功完成之后会触发的函数为所有的描述符(fd)绑定一个回调函数,一旦有数据访问就是触发该回调函数,回调函数将(fd)放到链表中,函数判断链表是否为空,且最大启动项没有限额
现实生活举例:班里三十个同学在考试,谁先做完交卷都要通过按钮来活动,一旦谁按了按钮老师桌子上的灯就会变红,并显示要交卷子的学生的名字,这样就可以对应学生是谁收卷子,也可以支持同时有很多人交卷子
(5)selectors模块:是可以实现IO多路复用机制,它具有根据平台选出最佳的IO多路机制,比如在win的系统上他默认的是select模式而在linux上它默认的epoll。
服务端代码:
import selectors #封装了一些相应的操作 import socket sel = selectors.DefaultSelector() #通过selectors模块下的DefaultSelector这个类拿到根据操作系统做判断取一个最好的I/O多路方法sel这个对象 #第六步:运行accept函数 def accept(sock, mask): #接收了sock和mask conn, addr = sock.accept() #sock.accept接收此时链接服务端的socket对象拿到conn和addr print('accepted', conn, 'from', addr) conn.setblocking(False) #设置成非阻塞 sel.register(conn, selectors.EVENT_READ, read) #把conn跟read做绑定后程序跳到while循环 #第十二步:运行read函数 def read(conn, mask): #接收了conn和mask try: #加异常防止客户端突然断开 data = conn.recv(1000) if not data: #如果接收到了数据 raise Exception print('客户端发来的内容是:', repr(data), '客户端信息是:', conn) conn.send(data) #给客户端返回一条数据 except Exception as e: print('断开的客户端信息是:', conn) sel.unregister(conn) #如果没有接收到数据做一个关闭解除 conn.close() sock = socket.socket() #创建sock对象 sock.bind(('localhost', 8080)) #绑定 sock.listen(100) #监听 sock.setblocking(False) #设置非阻塞 ##register注册完成绑定功能 sel.register(sock, selectors.EVENT_READ, accept) #把sock跟accept做绑定 print("服务端启动.....") while True: #第一步:程序启动后走while循环 #第七步:运行完accept函数后到这里 #所有操作围绕一个对象sel核心对象展开的 events = sel.select() #第二步:调用sel,监听的内容sock封装到events对象里 #第八步:如果客户端发过来此刻监听的内容就有变化有俩个对象sock和conn封装到events对象里 for key, mask in events: #第三步:for循环events(可迭代对象)拿到key和mask #第九步:for循环events(可迭代对象)拿到key和mask callback = key.data #第四步:当前key.data是accept函数赋值给callback #第十步:当前key.data是read函数赋值给callback #key.fileobj是拿到的监听的对象。 callback(key.fileobj, mask) #第五步:运行callback执行accpt函数 #第十一步:运行callback执行read函数里面放到的之前链接相应的文件描述符conn
客户端代码:
import socket sk=socket.socket() sk.connect(("127.0.0.1",8080)) while 1: inp=input(">>>") sk.send(inp.encode("utf8")) #客户端给服务端发消息 data=sk.recv(1024) #客户端接收服务端返回的消息 print(data.decode("utf8"))
客户端打印结果:
>>>xixi
xixi
服务端打印结果:
服务端启动.....
accepted <socket.socket fd=408, family=AddressFamily.AF_INET, type=SocketKind.SOCK_STREAM, proto=0, laddr=('127.0.0.1', 8080), raddr=('127.0.0.1', 52674)> from ('127.0.0.1', 52674)
客户端发来的内容是: b'xixi' 客户端信息是: <socket.socket fd=408, family=AddressFamily.AF_INET, type=SocketKind.SOCK_STREAM, proto=0, laddr=('127.0.0.1', 8080), raddr=('127.0.0.1', 52674)>
断开的客户端信息是: <socket.socket fd=408, family=AddressFamily.AF_INET, type=SocketKind.SOCK_STREAM, proto=0, laddr=('127.0.0.1', 8080), raddr=('127.0.0.1', 52674)>
4.异步IO:从开始到信号通知整个过程中不能有一点阻塞是异步存在
(1)用户进程发起read操作之后,立刻就可以开始去做气他的事。而另一方面,从kernel的角度,当它受到一个osynchronous read之后,首先它会立刻返回,所以不会对用户进程产生任何block。然后,kernel会等待数据准备完成,然后将数据拷贝到用户内存,当这一切完成之后,kernel会给用户进程发送一个signal,告诉它read操作完成了
四.四种I/O模型的区别
1.阻塞I/O和非阻塞I/O的区别
(1)阻塞I/O是全程阻塞的
(2)非阻塞I/O是在监听的时候是非阻塞的
2.同步IO操作和异步IO操作的区别
(1)同步I/O操作是在IO操作之前有阻塞发生
(2)异步I/O操作是没有引起任何的阻塞发生