一、概述
1.1 引入
传统编程风格:按顺序执行代码块,它是线性的 开始-代码A---代码B--...--结束,为高效出现事件驱动
def f():
pass
while 1:
鼠标检测
f()----会一直监听鼠标占用CPU,所以很占CPU,为减少CPU资源才用事件驱动
事件驱动例如:
<p onclick="func()">点我啊</p>-----点这个会触发某个事件函数,是js方式,js是事件驱动的方式,这步就是鼠标单击,
鼠标点一次前端内部就会有个队列来Put这个事件,来一个Put一个,处理就有线程用get,队列默认是先进先出
1.2 什么是IO?
1.内存分为用户空间与内核空间:用户空间内存与内核内存是分开的,用户空间不能用内核的空间
2.进程上下文切换
3.进程阻塞:进程阻塞动作是进程本身发用的阻塞,如套接字accept,这个阻塞会把CPU释放,等IO数据过来才能开始获取CPU权限
阻塞的状态是不占CPU的,CPU会交出去
4.文件描述符:是一个非负整数,是一个内核表里维护的一个套接字表,所以处理套接字就是处理fd(文件描述符)
import socket print(socket.socket())-----socket socket fd=220 family=AddrassFamily type=....(其中fd是文件描述符) 用户空间 内核空间 socket.socket() 内核与用户空间用fd来交互 | 用户的进程会问内核空间的fd数据有没有准备好 fd
5.过程描述
客端数据------->用户内存--复制--内核内存(能使用网卡IO) <> (能使用网卡IO)内核内存->复制到--->用户内存--服务器
网络IO实例:一个网络IO涉及二个系统对象一个调用IO的process另一个就是系统内核,当发生read操作时,有二个过程
1.待数据准备(服等待客户端发数据) 2.将数据从内核拷到进程中(服拿到数据从内核态给用户态)
二、IO的四种模型
1.阻塞IO:---有阻塞 sk.accept()---这个就是阻塞IO 服务端(1.通过方法bind listen accept发系统调用 2.conn.recv就开始等待数据(阻塞) 3.当服端拿到数据后从内核给用户态conn.send才行(这个过程也在阻塞,所以二个过程都阻塞,把进程阻塞住了,数据不来不向下走) 2.非阻塞IO----有阻塞 服端:(1.进程不断的发system call,有数据就拿没数据就继续向下走,一会再来问,这个过程它有CPU权限因它要看数据用到CPU--这个过程会有延时, 每次来问时不一定能看到,下次再来看到的是上次的就是延时,这个频率是人为可控的 2.当第n次拿到数据后,需要从内核复核到用户态(这个过程是让系统把内核数据给用户态这个是阻塞的,没有CPU权限)) 3.IO多路复用(事件驱动IO或同步):--有阻塞,是事件驱动IO(select与epoll都是多路复用的机制,nginx就是用epoll实现的,nginx就有很多连接过来能监听多个客端的文件描述符,进程与内核通信就是靠文件描述符) 服端:1.select方法或epoll方法发起系统调用,kernel等待数据,当数据来了后(阻塞了等待数据进程不能用CPU了),会通知这个调用,这时就有CPU再发系统调用 让把内核数据给用户内存 2.再次发系统调用让内核把数据复制给用户态(以上二种方法只有一次系统调用)(也阻塞了等待数据进程不能用CPU了) 优点:解决并发问题,select或epoll可以连接多个文件描述符来实现并发,以上二种方法不能连接多个对象,一个服务器可有多个套接字同时被监听, 不同端口被不同客户端来连并能响应,只有客户端来连时才会有对象返回,对象返回后才会有数据传来 (第一步返回对象,也同conn,addr=sk.accept()做系统调用客户端连后才会有对象返回,第二步有数据返回) 4.异步IO:--完全没阻塞,实现很难 服端:1.aio_read方法发起系统调用(发给内核,等待数据,内核拿到数据复核给用户,这时内核复制组用户后直接通知进程了,这二个过程中进程未阻塞) 整个过程没有阻塞,以三种内核不会主动通知,可自己去看
三、IO多路复用(同步IO)创建使用
1.select方法:如何实现事件驱动的 监听多个文件描述符时,循环监听所有的文件描述符fd,不断的循环问哪个fd的数据准备好就会把数据复制到用户空间,这个缺点fd很多时不断的循环问费时费资源,而且它最多只能监听1024个文件描述符 2.poll方法:与select唯一的区别是,poll只解决了select的监听数据问题,能监听的更多但还是这样轮循。 3.epoll方法:在poll与select的基础上解决了轮循问题,只要一个fd数据准备好,epoll就知道是谁直接把数据复制到用户空间就行,不需要一个个问是谁的数据好了 节省cpu的时间,这是多个fd,而对于每一个fd来说二段还是阻塞的 实例非阻塞IO: server.py import socket sk=socket.socket() sk.bind(('127.0.0.1',8000)) sk.listen(3) sk.setblocking(False)----1.问下面的系统调用有没有数据,这个是不断的去问 while 1: try: conn,addr=sk.accept()---2.发系统调用,没有数据就报错,这个进程再发系统调用,这个是循环不断问 print(addr) data=conn.recv(1024) print(data.decode('utf8')) conn.sendall(data) conn.close() except Exception as e: print('没有数据',e)--上步报错就不断打印这个提示 time.sleep(3) client.py import socket sk1=socket.socket() sk1.connect(('127.0.0.1',8000)) while 1: inp=input('>>>')--是unicon编码的 sk1.sendall(hello.encode('utf8')) data=sk1.recv(1024) print(data.decode('utf8')) 实例2:IO多路复用(通过select函数监听多个对象)----看视频 server.py import socket import select sk1=socket.socket() sk1.bind(('127.0.0.1',8080)) sk1.listen(3) sk2=socket.socket() sk2.bind(('127.0.0.1',8081)) sk2.listen(3) while 1:---要循环监听 r,w,e=select.select([sk1,sk2],[],5)----监听socket对象,返回值有三个,如果有客户端来连就会有对象返回存在r里,r就是客户端对象与IP+端口,所以r就是sk1或sk2 for obj in r: conn,addr=obj.accept()----哪个客户端连就返回哪个客户端的conn,r是服端的socket对象,conn是客端的socket对象所以二个fd是不同的,r调用accept才能打印出conn对象,conn不是r分出来的而是它调用accept方法得到的 conn.send(hello.encode('utf8')) 问题:当只有一个sk时,当客户端连上服后r=sk,当客户端第二次发数据时就进不了for了,因r没变,如何做到客户端数据变就向下走 select也监听conn时,当conn有变化时select就触发向下走,conn与sk都是对象,实例如下 client.py import socket sk=socket.socket() sk.connect(('127.0.0.1',8080)) while 1: data = sk.recv(1024) print(data.decode('utf8')) inp=input('>>>') sk.sendall(inp.encode('utf8')) 为什么要能监听多个socket对象? IO多路复用的水平触发(某种状态一直触发,1或0),边缘触发(只有高低电平变化就触发,0变1或1变0),eg:select.select()---只要里面有数据就一直触发,所以select是水平触发,只有把数据用掉才不会触发 而epoll二种触发方式都有,数据一直在或数据变化都触发 r,w,e=select.select([sk1,sk2],5):监听5s,超时时间 实例: while 1: inputs,outputs,errors=selec.select([sk,conn],[],3)----监听后二种方式才能向下触发 当sk不变时不同客户端进来也会向下走,一个服可跟多少客户端对话 对比阻塞IO与IO多路复用过程: 阻塞IO二个阻塞过程:1.等待数据是send与recv来等的 2.复制数据到用户也是这二个发的 IO多路复用:1.等待数据是select来等的 2.复制数据是accept来复制的,它做的是第二次系统调用,这里的数据是conn,只有select执行accept才能执行