一、IO模型简介
1.什么是IO模型
1.模型:即解决某个问题的固定套路
2.IO:输入输出
3.IO模型:就是解决IO操作时需要等待的问题的模型
2.IO的特点
1.输入/输出数据所需要的时间对于CPU而言非常长
2.IO操作所需要的等待时间,会让CPU处于闲置状态,造成资源浪费
3.注意:IO有很多种类型,如socket网络IO、内存到内存的copy、键盘输入等等,其中socket网络IO需要等待的时间最长,也是我们学习的重点
3.为什么需要IO模型
可以在等待IO操作的过程中,利用CPU做别的事
二、socket网络IO
1.操作系统的两种状态:
1.内核态(kernel):等待数据准备(例如,数据从网络到网卡,最后拷贝到系统内核的缓存中)
2.用户态(process/thread):将数据从内核拷贝到进程中(操作系统接收完数据后,把数据从操作系统缓冲区中,copy到应用程序的缓冲区,操作系统从内核态转化为用户态)
2.涉及的两个步骤
1.wait_data(内核态)
2.copy_data(用户态)
3.例如,recv和accept需要经历wait_data --> copy_data;send只需要经历copy_data
三、IO模型详解
1.阻塞IO
1.默认情况下,我们写的普通的TCP程序就是阻塞IO模型
2.该模型的方式,就是当执行recv/accept 会进入wait_data的阶段,
**3.而在这个阶段中,进程会主动调用一个block指令,进程进入阻塞状态,同时让出CPU的执行权,操作系统就会将CPU分配给其它的任务,从而提高了CPU的利用率 **
**4.当recv/accept的数据到达时,首先会从内核将数据copy到应用程序缓冲区,并且socket将唤醒处于自身的等待队列中的所有进程(某个文件中有一个装着所有进程的列表,通过遍历列表来唤醒所有进程) **
5.因此,之前使用多线程、多进程 完成的并发 其实都是阻塞IO模型 每个线程在执行recv时,都会卡住
# 服务端
from socket import *
server = socket()
server.bind(('127.0.0.1', 8000))
server.listen()
while True:
client, address = server.accept()
while True:
msg = client.recv(1024)
client.send(msg.upper())
2.非阻塞IO
**1.非阻塞IO模型与阻塞模型相反 ,在调用recv/accept 时都不会阻塞当前线程 **
2.该模型在没有数据到达时,会跑出异常,我们需要捕获异常,然后继续不断地询问系统内核,直到数据到达为止
3.该模型会大量的占用CPU资源做一些无效的循环, 效率低于阻塞IO
4.使用方法: 将原本阻塞的socket 设置为非阻塞
from socket import *
import time
s = socket()
s.bind(('127.0.01', 8000))
# 设置为非阻塞模式(socket中自带的方法)
s.setblocking(False) # 默认为True,所有要将状态改为False
s.listen()
# 保存所有的客户端
cl = list()
# 用于保存需要发送的数据
msgl = list()
while True:
try:
c,addr = s.accept() # 完成三次握手
cl.append(c)
except BlockingIOError:
# 代码执行到这里说明,没有连接需要处理
# 则可以利用CPU处理收发数据的任务
for c in cl[:]: # 只处理接收数据
try:
data = c.recv(1024)
if not data:
raise ConnectionResetError
msgl.append((c, data.upper())) # 将客户端和接收的数据打包
# c.send(data.upper()) # 不能直接发送,因为当内核缓冲区满了,就会抛出异常
except BlockingIOError:
pass # 如果遇到阻塞则说明,数据客户端没再发数据
except ConnectionResetError:
# 如果遇到这个异常,说明客户端自动关闭了
# 所有服务器也需要关闭连接,并删除这个客户端
c.close()
cl.remove(c)
# 只处理发送数据
for i in msgl[:]:
try:
i[0].send(i[1])
msgl.remove(i) # 一个客户端发送完它的信息则移除
except BlockingIOError:
pass
except ConnectionResetError:
# 关闭连接
i[0].close()
# 删除数据
msgl.remove(i)
# 删除链接
cl.remove(i[0])
3.多路复用IO
**1.属于事件驱动模型 **
2.多个socket使用同一套处理逻辑
1.如果将非阻塞IO,比喻是点餐的话,相当于你每次去前台,照着菜单挨个问个遍,循环往复,效率很低
2.多路复用,直接问前台哪些菜做好了,前台会给你返回一个列表,里面就是已经做好的菜
3.多路复用对比非阻塞 ,多路复用可以极大降低CPU的占用率
3.对比阻塞或非阻塞模型,增加了一个select,来帮我们检测socket的状态,从而避免了我们自己检测socket带来的开销
4.select会把已经就绪的放入列表中,我们需要遍历列表,分别处理读写即可
5.注意:多路复用并不完美 因为本质上多个任务之间是串行的,如果某个任务耗时较长将导致其他的任务不能立即执行,多路复用最大的优势就是高并发
import socket
import time
import select
s = socket.socket()
s.bind(('127.0.0.1', 8000))
# 在多路复用中,阻塞与非阻塞没有区别,因为select函数会阻塞到有数据到达为止
s.setblocking(True)
s.listen()
# 待检测是否可读
read_list = [s] # 将服务端放入列表,发给select去判断是否有数据
# 待检测是否可写
write_list = [] # select会返回可写列表,用于哪些客户端需要回应
# 待发送的数据,里面是客户端和发给该客户端的内容
msgs = {}
print('start...')
while True:
# 通过select中的select方法返回可读的对象和可写的对象(也就是服务端和客户端都可以读或写)
read_ables, write_ables, _ = select.select(read_list, write_list, [])
# 如果服务端在可读的对象中,说明有数据传输(IO操作)
for obj in read_ables: # 拿出所有可以读数据的socket对象(有可能是服务器,也有可能是客户端)
if s == obj: # 如果对象是服务端,说明有客户端连接进来了
client, addr = s.accept() # 获取客户端对象和地址
read_list.append(client) # 将与服务端连接的客户端添加到可读列表中
else: # 如果是客户端对象可读,说明服务端需要利用客户端对象接收数据了
try:
data = obj.recv(1024)
if not data: raise ConnectionResetError
print(data)
# 将发送信息的客户端添加到可写列表中
write_list.append(obj)
if obj in msgs: # 如果信息字典中有此客户端对象
msgs[obj].append(data) # 则只需要将它发送的信息添加到信息字典中所对应的列表
else:
msgs[obj] = [data] # 否则要创建一个键值对,用于表示客户端对象所对应它发送的信息
except ConnectionResetError:
obj.close() # 当断开连接时,关闭客户端
read_list.remove(obj) # 并移除客户端
# 处理可写内容,也即是服务端回复客户端
for obj in write_ables: # 取出所有的可写对象
msg_list = msgs.get(obj) # 在信息字典中取出要回复的信息列表
if msg_list: # 如果有信息列表,则发送
for m in msg_list: # 取出该客户端发送的所有的信息
try:
obj.send(m.upper()) # 通过客户端对象发送信息
except ConnectionResetError:
obj.close() # 当客户端断开连接时,服务端也断开连接
write_list.remove(obj) # 当客户端退出之后,移除该客户端对象
msgs.pop(obj) # 发送完该对象的信息,就从信息字典中删除该客户端及其信息
write_list.remover(obj) # 发送完一个对象的信息就将它删除
4.异步IO
非阻塞IO不等于异步IO,,因为非阻塞IO中的copy的过程是一个同步任务,会卡住当前线程,而异步IO,是发起任务后 就可以继续执行其它任务,当数据copy到应用程序缓冲区,才会给你的线程发送信号 或者执行回调
asyncio
5.信号驱动IO
简单的说就是,当某个事情发生后,会给你的线程发送一个信号,你的线程就可以去处理这个任务
不常用,原因是 socket的信号太多,处理起来非常繁琐
6.不要在迭代的过程中操作元素
# a = [1,2,3,4,5]
# for i in a[:]: # [:] 会复制一份一模一样的列表
# print(i)
# if i == 2:
# a.remove(i)
# # a.append(10)
# print(a)
# a = {"a":1,"b":2}
# for k in a:
# if k == "a":
# a.pop(k)
a = [(1,2),(3,2),(100,21),(31,22),(33,22)]
index = 0
for i in a[:]:
print(i)
if index == 1:
a.remove(i)
index += 1
print(a)