IO模型
模型就是解决某个问题的套路,IO模型主要是用来解决IO问题的
IO问题:输入输出的问题
例如我需要一个用户名来执行登录操作,问题是用户名需要输入,输入需要耗时,如果输入没有完成,
后续的逻辑代码就无法继续执行,这种默认的处理方式就是阻塞IO模型
网络IO中必经的两个阶段
1、wait_data:等待数据从对方发到系统的缓存区
2、copy_data:将数据从系统缓存区拷贝到应用的内存中
阻塞IO
阻塞IO模型指的是当遇到IO操作时,就将当前的线程阻塞住,cpu切换到其他线程执行,
一直等到IO操作结束,拿到想要的结果时,再唤醒刚刚的线程,将状态调整为就绪态
存在的问题
当执行到recv时,如果对象并没有发送数据,程序就阻塞了,没办法执行其他任务
解决方案
多线程或者多进程:当客户端并发量非常大的时候,服务器可能无法开启新的线程或者
进程,如果不对数量加以限制,服务器就崩溃了(无效率可言)
线程池或者进程池:对最大并发量加以了限制,保证服务器正常运行,但是问题时,
如果所有客户端都处于阻塞状态,那么这些线程也阻塞了,
后面再想进来访问也进不来
协程:使用一个线程处理所有的客户端,当一个客户端处于阻塞状态时,
可以切换至其他客户端任务
非阻塞IO
阻塞IO模型在执行 recv 和 accept 时 都需要经历wait_data
非阻塞IO模型即 在执行recv 和 accept 时,不会阻塞,可以继续往下执行
如何使用:
将server的blocking设置为False即设置非阻塞
import socket client = socket.socket() client.connect(("127.0.0.1",1688)) while True: msg = input(">>:").strip() if not msg:continue if msg == 'q':break client.send(msg.encode('utf-8')) print(client.recv(1024).decode('utf-8'))
将阻塞设置为非阻塞之后需要注意的几个点
1、accept recv send 几个函数都有可能出现阻塞状态,所以都需要抓取异常
2、需要不停的循环去问缓存区有没有数据,就需要把所有的客户端socket对象加到容器中,
不断遍历recv,如果有值就接收,没值就不管
3、回消息也应该把消息和对应的socket对象放到容器中,循环发送,如果发送成功,
就从消息容器中删除,如果发送失败,说明现在缓存区是满的,下次循环再发
4、客户端正常退出和异常退出都需要抓取异常,并且关闭socket对象,移除容器,
这时候就需要注意发消息 的容器中还有没有这个socket对象的消息,如果有,
一起删除,防止后续发出会抛异常
import socket server = socket.socket() server.bind(("127.0.0.1",8080)) server.listen() server.setblocking(False) # 把阻塞改为非阻塞 clients = [] # 创建容纳客户端socket对象的容器列表 msgs = [] # 创建一个保存要发送信息和socket对象对应的容器 while True: try: client,addr = server.accept() # 如果没人连接过来,就会被捕获异常 clients.append(client) except BlockingIOError: print("没人连接过来") # 接收消息,把消息保存起来 for c in clients[:]: try: data = c.recv(1024) # 没接收到消息就会捕获异常,接着往下执行 if not data: c.close() # 如果客户端正常退出,在这里关闭客户端socket对象,并且移除列表 clients.remove(c) continue # 在这里关闭移除完之后要continue一下,不然还会把socket对象添加到保存消息列表中 msgs.append((c,data)) except BlockingIOError: print("没有任务需要接收") except ConnectionResetError: c.close() clients.remove(c) # 其实在这关闭移除还需要注意一个点,那就是如果上面已经添加到保存列表去 # 然后对面强行中断连接的话也会报异常 # 发送消息 for m in msgs[:]: try: c,data = m c.send(data.upper()) # 如果缓存区满了,就会捕获异常,接着执行 msgs.remove(m) # 信息发送完之后把消息移除 except BlockingIOError: print("没有任务需要发送")
多路复用IO
在非阻塞IO模型中,我们要想实现并发,就必须一直询问操作系统缓存有没有数据或者能不能发送,
这样是非常耗cpu的
所以我们想了一个办法,就是让一个对象去统一检测,如果有socket对象可写或者可读,就把socket对象返出来
然后进行处理,这里需要用到的模块就是select模块
select模块中有一个select函数,这个函数可以实现帮我们检测列表中的socket对象是否可读或者可写
select监听的socket对象是有上限的,默认为1024个
使用方法
import socket client = socket.socket() client.connect(("127.0.0.1",8080)) while True: msg = input("msg:").strip() if not msg:continue if msg == 'q':break client.send(msg.encode('utf-8')) print(client.recv(1024).decode('utf-8'))
import socket import select server = socket.socket() server.bind(("127.0.0.1",8080)) server.listen() rlist = [server,] # 创建读的检测列表,server也是可读的socket对象,需要先加进来 wlist = [] # 创建写的检测列表 msgs = [] """ select模块中select函数中的几个参数 rlist: 里面存储检测是否可读(recv、accept)的socket对象 wlist: 里面存储检测是否可写(send)的socket对象 xlist: 存储你需要关注的异常条件,忽略 timeout:设置超时时间,如果超过设定时间还没有检测到可读或者可写,就返回空列表(一般不设置) """ while True: readable_list,writeable_list,_ = select.select(rlist,wlist,[]) # 循环可读的列表readable_list,有socket对象就读出来 # 读取数据 for soc in readable_list: # type:socket.socket # 这个可以让soc有socket对象的提示 try: if soc == server: # 判断如果是等待连接的,就接收连接,并把客户端socket对象加到检测列表中 client,addr = server.accept() rlist.append(client) else: # 如果不是server,那就说明是客户端的socket对象可读了,读取数据 data = soc.recv(1024) if not data: soc.close() # 检测到对面正常退出,那就把socket对象close并且移出检测列表 rlist.remove(soc) continue if soc not in wlist: # 判断如果socket对象不在检测可写列表中就添加,在就不管 wlist.append(soc) msgs.append((soc,data)) # 将socket对象和数据存到容器中 except ConnectionResetError: soc.close() rlist.remove(soc) # 抓捕到对面强退错误后,关闭socket对象,并且从rlist中移除 if soc in wlist: # 并且需要判断一下这个socket对象是否在可写的检测列表中,在就移除 wlist.remove(soc) # 循环可写的列表writeable_list,有socket对象就发送数据 # 发送数据 for soc in writeable_list: # type:socket.socket # 这里面不可能有server,所以不需要判断 for msg in msgs[:]: if soc == msg[0]: # 判断对象相同 soc.send(msg[1].upper()) msgs.remove(msg) # 发送完就可以把信息从保存列表中移除 wlist.remove(soc) # 如果这个socket对象需要发送的信息全部发送完之后,需要把他移出wlist检测 # 因为select检测send就是如果缓存区没满,就会给你返回可写 # 你如果不删除,会导致select会一直循环给你发可写信息
服务器端需要注意的几个细节
1、select.select()函数中几个参数的意思
你又没东西写)