一、网络IO的两个阶段:
IO:输入、输出 IO问题:程序在运行时,遇到IO操作时,就会等待任务结束,若任务还没完成,后续
的逻辑代码都无法执行。 解决: 当我们遇到IO操作时,此时处于阻塞状态,可以切换到其他任务,当IO结束再次切回。 IO操作: #1、输入操作:read、readv、recv、recvfrom、recvmsg共5个函数,如果会
阻塞状态,则会经理wait data和copy data两个阶段,如果设置为非阻塞则在
wait 不到data时抛出异常 #2、输出操作:write、writev、send、sendto、sendmsg共5个函数,在发送
缓冲区满了会阻塞在原地,如果设置为非阻塞,则会抛出异常 #3、接收外来链接:accept,与输入操作类似 #4、发起外出链接:connect,与输出操作类似
二、阻塞IO(blocking IO)
在执行IO操作时,要经历两个阶段: #1)等待数据准备 (Waiting for the data to be ready) #2)将数据从内核拷贝到进程中(Copying the data from the kernel to
the process) 而阻塞IO的特点就是在IO执行的两个阶段(等待数据和拷贝数据两个阶段)都被
block(阻塞)了。 在网络IO中,最耗时的阶段就是等待数据的过程,其中还涉及到网速问题,而本地
copy则由于是操作本地数据会比较快,所以不做考虑。 实际上,除非特别指定,几乎所有的IO接口 ( 包括socket接口 ) 都是阻塞型的。
网络编程中,就有很大问题,如在调用recv(1024)的同时,线程将被阻塞,在此期
间,线程将无法执行任何运算或响应任何的网络请求。 解决: #在服务器端使用多线程(或多进程)、“线程池”和“连接池”。 问题: 多线程模型可以方便高效的解决小规模的服务请求,但面对大规模的服务请求,多线程
模型也会遇到瓶颈,可以用非阻塞接口来尝试解决这个问题。
三、非阻塞IO(non-blocking IO)
由于程序执行过程中,默认IO阻塞,这是要设置setblocking(False),变为非
阻塞。 在非阻塞式IO中,用户进程其实是需要不断的主动询问kernel数据准备好了没有。 服务器: import socket import time server = socket.socket() server.bind(("192.168.13.103",1688)) server.listen() server.setblocking(False) # 默认为阻塞 设置为False 表示非阻塞 # 用来存储客户端的列表 clients = [] # 用来存储需要发送的数据和客户端对象 msgs = [] # 链接客户端的循环 while True: try: client,addr = server.accept() # 接受三次握手信息 # print("来了一个客户端了.... %s" % addr[1]) # 有人链接成功了 clients.append(client) except BlockingIOError as e: # print("还没有人连过来.....") time.sleep(0.01) # 收数据的操作 for c in clients[:]: try: # 可能这个客户端还没有数据过来 # 开始通讯任务 data = c.recv(2048) if not data: c.close() clients.remove(c) #c.send(data.upper()) # 如果碰巧缓存区满了
这个数据就丢失了 # 由于此处捕获了异常 所以应该单独来处理发送数据 msgs.append((c,data)) except BlockingIOError as e: print("这个客户端还不需要处理.....",) except ConnectionResetError: # 断开后删除这个客户端 c.close() clients.remove(c) # 发送数据的操作 for i in msgs[:]: try: c,msg = i c.send(msg.upper()) msgs.remove(i) # 如果发送成功! 删除这个数据 except BlockingIOError: pass 客户端: import socket client = socket.socket() client.connect(("127.0.0.1",1688)) while True: msg = input("msg:") if not msg:continue client.send(msg.encode("utf-8")) print(client.recv(2048).decode("utf-8"))
非阻塞IO: 优点: 能够在等待IO任务完成的时间内做其他的任务 缺点: #1. 循环调用recv()将大幅度推高CPU占用率。 #2. 任务完成的响应延迟增大了,因为每过一段时间才去轮询一次read操作,
而任务可能在两次轮询之间的任意时间完成,导致效率变低。
四、select实现多路复用
在多路复用IO中,使用select监听整个socket,此时整个进程都是阻塞状态,
若任何一个socket中的数据准备了,select就会有返回结果。 服务器: import socket import time import select server = socket.socket() server.bind(("127.0.0.1",1688)) server.listen() # server.setblocking(False) # 默认为阻塞 设置为False 表示
非阻塞 """ 参数1 rlist 里面存储需要被检测是否可读(是否可以执行recv)的
socket对象 参数2 wlist 里面存储需要被检测是否可写(是否可以执行send)的
socket对象 参数3 xlist 存储你需要关注异常条件 忽略即可 参数4 timeout 检测超时时间 一段时间后还是没有可以被处理的socket
那就返回空列表 返回值: 三个列表 1 已经有数据到达的socket对象 2 可以发送数据的socket对象 怎么可以发 缓冲区没有满 3 忽略.... """ rlist = [server,] wlist = [] # 要发送的数据和socket msgs = [] while True: ras,was,_ = select.select(rlist,wlist,[]) # 阻塞直到
socke可读或可写 # 处理可读的socket for s in ras: if s == server: client,addr = server.accept() rlist.append(client) else: try: # 收数据 data = s.recv(2048) if not data: raise ConnectionResetError() wlist.append(s) # s.send(data.upper()) # 将要发送的数据和socket 保存起来 msgs.append((s,data)) except ConnectionResetError: s.close() rlist.remove(s) if s in wlist:wlist.remove(s) # 处理可写的socket for s in was: for msg in msgs[:]: if msg[0] == s: s.send(msg[1].upper()) # 发送成功之后 删除已经无用的数据 并且需要将socket
从wlist列表中删除 # 不删除会造成死循环 因为socket 一直处于可写状态 msgs.remove(msg) wlist.remove(s) 客户端: import os import socket client = socket.socket() client.connect(("127.0.0.1",1688)) while True: msg = input("msg:") if not msg:continue client.send(msg.encode("utf-8")) print(client.recv(2048).decode("utf-8"))
使用多路复用IO: 1.如果处理的连接数不是很高的话,效率并不高,它处理的是处理更多的连接。 2.在多路复用模型中,对于每一个socket,一般都设置成为non-blocking,但是,
如上图所示,整个用户的process其实是一直被block的。只不过process是被
select这个函数block,而不是被socket IO给block。 3.select最多能检测1024个socket 超出直接报错 这是select自身设计的
问题 4.相比其他模型,使用select()占用资源少,不消耗太多CPU,同时能够为多
客户端提供服务。