3.3.4 IO多路复用
-
定义
系统同时监控多个IO事件,当哪个IO事件准备就绪就执行哪个IO事件。以此形成可以同时处理多个IO的行为,避免一个IO阻塞造成其他IO均无法执行,提高了IO执行效率。
-
-
在单进程的状态下,IO想要实现跳过阻塞部分去执行非阻塞部分来提高程序的执行的效率,应用层是没有这样功能,需要借助于操作系统的轮循机制,不断寻找没有阻塞的部分去先执行。
-
2个具体方案
-
select方法 : Windows Linux Unix
-
epoll方法: Linux --效率略高于select
-
-
select 方法
import select rs, ws, xs=select(rlist, wlist, xlist,[timeout]) 功能: 监控IO事件,阻塞等待IO的发生 参数: rlist 列表 读IO列表,添加等待发生的或者可读的IO事件 读例如:read recv accept ---别的地方往程序里面搞东西的,经常被动阻塞 wlist 列表 写IO列表,存放要可以主动处理的或者可写的IO事件 写例如:send write---程序往别的地方输出信息,经常主动不阻塞,实际很少用它。 xlist 列表 异常IO列表,存放出现异常要处理的IO事件,基本在Linux下无用,系统不会帮你捕捉异常事 件,需要在我们应用层捕捉。因此基本不用他。 timeout 超时时间---不写默认死等 返回值: rs 列表 rlist中准备就绪的IO ws 列表 wlist中准备就绪的IO xs 列表 xlist中准备就绪的IO
""" IO 多路复用方法 select """ from select import select from socket import * # 准备一些IO操作对象 file = open("../day15/my.log", 'rb') # open创建的对象比较特殊,同时具备读与写事件 udp = socket(type=SOCK_DGRAM) # 具有写事件已经就绪 tcp = socket() tcp.bind(("0.0.0.0", 8888)) tcp.listen(5) # 没有客户端连接就一直阻塞 # 监控IO print("开始监控") rs, ws, xs = select([tcp, file], [udp, file], []) print("rlist:", rs) print("wlist:", ws) print("xlist:", xs)
from socket import * # 服务器地址 ADDR = ("127.0.0.1",8888) tcp_socket = socket() tcp_socket.connect(ADDR) while True: msg = input(">>") if not msg: break tcp_socket.send(msg.encode()) data = tcp_socket.recv(1024) print("From server:",data.decode()) # 关闭 tcp_socket.close()
""" 单进程基于select 的IO多路复用并发模型 重点代码 !! 目标 : 服务端能够同时应对多个客户端发消息和连接 思路 : 将监听套接字与连接套接字一起监控 """ from socket import * from select import select HOST = "0.0.0.0" PORT = 8888 ADDR = (HOST, PORT) # 监控列表 rlist = [] wlist = [] xlist = [] # 处理客户端连接 def connect_client(sock): connfd, addr = sock.accept() print("Connect from", addr) connfd.setblocking(False) # 设置套接字为非阻塞IO rlist.append(connfd) # 具体客户端发送请求 def handle(connfd): data = connfd.recv(1024) if not data: rlist.remove(connfd) # 不关注 connfd.close() return print(data.decode()) connfd.send(b"OK") # 启动服务函数 def main(): # 创建监听套接字 sock = socket() sock.bind(ADDR) sock.listen(5) sock.setblocking(False) # 与非阻塞IO配合 print("Listen the port %d" % PORT) rlist.append(sock) # 初始关注监听套接字 # 循环接收监控IO发生 while True: rs, ws, xs = select(rlist, wlist, xlist) for r in rs: if r is sock: # 判断对象用is最合适 connect_client(r) # 处理连接 else: handle(r) # 处理请求 if __name__ == '__main__': main()
""" 加入写到wlist """ from socket import * from select import select HOST = "0.0.0.0" PORT = 8888 ADDR = (HOST, PORT) # 启动服务函数 def main(): # 创建监听套接字 sock = socket() sock.bind(ADDR) sock.listen(5) sock.setblocking(False) # 与非阻塞IO配合 print("Listen the port %d" % PORT) # 监控列表 rlist = [sock] # 初始关注监听套接字 wlist = [] xlist = [] # 循环接收监控IO发生 while True: rs, ws, xs = select(rlist, wlist, xlist) for r in rs: if r is sock: connfd, addr = r.accept() # 处理连接 print("Connect from", addr) connfd.setblocking(False) rlist.append(connfd) else: data = r.recv(1024) if not data: rlist.remove(r) # 不关注 r.close() continue print(data.decode()) # r.send(b"OK") wlist.append(r) # 加入写到wlist for w in ws: w.send(b"OK") wlist.remove(w) # 移除 if __name__ == '__main__': main()
-
epoll方法
import select ep = select.epoll() 功能 : 创建epoll对象 返回值: epoll对象
ep.register(fd,event) 功能: 注册关注的IO事件 参数:fd 要关注的IO event 要关注的IO事件类型 常用类型EPOLLIN 读IO事件(rlist) EPOLLOUT 写IO事件 (wlist) EPOLLERR 异常IO (xlist) e.g. ep.register(sockfd,EPOLLIN|EPOLLERR)#关注多个事件用 | ep.unregister(fd) 功能:取消对IO的关注 参数:IO对象 或者IO对象的fileno(即文件描述符) 文件描述符 : Linux操作系统会给每个IO对象分配一个 不重复的整数编号,这个编号就是文件描述符
events = ep.poll() 功能: 将关注的IO提交监控,阻塞等待监控的IO事件发生 返回值: 返回发生的IO events格式 [(fileno,event),()....] 每个元组为一个就绪IO,其中元组第一项是该IO的fileno,第二项为该IO就绪的事件类型EPOLLIN,EPOLLOUT , EPOLLERR
""" 基于epoll 的IO多路复用并发模型 重点代码 !! 目标 : 服务端能够同时应对多个客户端发消息和连接 思路 : 将监听套接字与连接套接字一起监控 """ from socket import * from select import * HOST = "0.0.0.0" PORT = 8888 ADDR = (HOST, PORT) # 查找字典 时刻与关注的IO对象一致 {fileno:object} map = {} # 处理客户端连接 def connect_client(ep, fd): connfd, addr = map[fd].accept() print("Connect from", addr) connfd.setblocking(False) # 添加新的关注 设置边缘触发 ep.register(connfd, EPOLLIN|EPOLLET) map[connfd.fileno()] = connfd # 维护字典 # 具体客户端发送请求 def handle(ep, fd): data = map[fd].recv(1024) if not data: ep.unregister(fd) map[fd].close() del map[fd] # 从字典中移除 return print(data.decode()) map[fd].send(b"OK") # 启动服务函数 def main(): # 创建监听套接字 sock = socket() sock.bind(ADDR) sock.listen(5) sock.setblocking(False) # 与非阻塞IO配合 print("Listen the port %d" % PORT) # 创建epoll对象 ep = epoll() ep.register(sock, EPOLLIN) # 初始关注监听套接字 # 查找字典 时刻与关注的IO对象一致 {fileno:object} map[sock.fileno()] = sock # 循环接收监控IO发生 while True: events = ep.poll() # 监控函数 [(),()] print("你有新的IO需要处理哦",events) for fd, event in events: if fd == sock.fileno(): connect_client(ep, fd) # 处理连接 # else: # handle(ep, fd) # 处理请求 if __name__ == '__main__': main()
""" IO 多路复用方法 epoll """ from select import * from socket import * # 准备一些IO操作对象 file = open("../day15/my.log",'rb') udp = socket(type=SOCK_DGRAM) tcp = socket() tcp.bind(("0.0.0.0",8888)) tcp.listen(5) # 查找字典 {fileno : object} map = {} ep = epoll() ep.register(tcp,EPOLLIN) # 读事件 map[tcp.fileno()] = tcp # 添加到字典 # 监控IO print("开始监控") events = ep.poll() # 阻塞等待 print("events:",events) # [(5,1)] map[5].accept()
-
select 方法与epoll方法对比
-
epoll 效率比select要高
-
epoll 同时监控IO数量比select要多
-
epoll 支持EPOLLET触发方式
-
3.3.5 IO并发模型
利用IO多路复用等技术,同时处理多个客户端IO请求。
-
优点 : 资源消耗少,能同时高效处理多个IO行为
-
缺点 : 只针对处理并发产生的IO事件
-
适用情况:HTTP请求,网络传输等都是IO行为,可以通过IO多路复用监控多个客户端的IO请求。
-
网络并发服务实现过程
【1】将套接字对象设置为关注的IO,通常设置为非阻塞状态。
【2】通过IO多路复用方法提交,进行IO监控。
【3】阻塞等待,当监控的IO有事件发生时结束阻塞。
【4】遍历返回值列表,确定就绪的IO事件类型。
【5】处理发生的IO事件。
【6】继续循环监控IO发生。
IO与进程线程对比
两者表面上都是服务端应对多个服务端,前者是创建多进程线程实现,适合大型的底层框架结构,资源消耗多。后者是单进程,只用一个计算机内核,需要操作系统功能支持,利用等待阻塞的时间来完成,资源消耗少,轻量级,适合处理网络收发事件。工作中两者配合用,例如来一个用户开启一个进程专门应对客户端,客户端请求复杂情况下,在这个进程里面用IO多路复用减少多个请求的阻塞时间。
4. web服务
4.1 HTTP协议
4.1.1 协议概述
-
用途 : 网页获取,数据的传输
-
特点
-
应用层协议,选择使用tcp进行数据传输
-
简单,灵活,很多语言都有HTTP专门接口
-
有丰富的请求类型
-
可以传输的数据类型众多
-
4.1.2 网页访问流程
-
客户端(浏览器)通过tcp传输,发送http请求给服务端
-
服务端接收到http请求后进行解析
-
服务端处理请求内容,组织响应内容
-
服务端将响应内容以http响应格式发送给浏览器
-
浏览器接收到响应内容,解析展示
前端工程师用JavaScript(写行为动作)与HTML(写内容)与CSS(写样式),给到服务器的某个位置保存起来,因此网页就是一个文件。DNS是实现域名解析,把百度转换成IP地址,
前情回顾 1. 多进程多线程并发模型 用函数编写 / 用类编写 2. ftp文件服务 * 通信协议 响应: 响应情况 响应信息 * 请求和处理请求的模型 客户端发送请求-> 等待响应-> 根据响应不同分情况讨论 接收请求->处理请求->根据处理情况不同发送响应 3. IO 模型 输入(读) 输出(写) IO 密集 : IO多 耗时长 多阻塞 cpu消耗少 计算密集 : 计算多 cpu消耗多 无阻塞 耗时短 4. 阻塞 IO 和 非阻塞 IO 阻塞 IO : 默认 效率最低 非阻塞IO : 将阻塞 --> 不阻塞 sock.setblocking() sock.settimeout(3) cookie :Python支持位运算 位运算: 将整数转换为二进制数再按照位进行计算 | 按位或 14 | 17 1110 10001 11111 --> 31 & 按位与 14 & 17 1110 10001 00000 --> 0 Cookie : 文件描述符 : Linux操作系统会给每个IO对象分配一个 不重复的整数编号,这个编号就是文件描述符 >=0的 训练 : 将select_server 改写为使用epoll方法来 实现. select : 支持系统多,使用简单,只有水平出发 epoll : 效率比select高 , 同时监控IO数量多 默认水平出发,但是支持边缘出发 水平触发 : 当有IO事件发生时如果不处理则一直提醒 作业 : 1. 聊天室和ftp文件服务器 最少二选一写一下 2. 第二阶段 做一个 思维导图 把知识点梳理