目录
介绍
两个过程:
- 等待数据;
- 将数据从系统内存拷贝到用户进程中.
阻塞IO
等待数据:当用户进程调用了recvfrom
这个系统调用,操作系统内核就开始了准备数据,但是一般数据在一开始还没有到达操作系统内存中,此时整个进程会被阻塞。
拷贝数据:当操作系统内存数据准备好了,就会拷贝到用户内存,然后返回结果,用户进程才解除block
的状态,重新运行起来。
阻塞IO:在IO执行的两个阶段(等待数据和拷贝数据两个阶段)都被block
了。几乎所有的程序员第一次接触到的网络编程都是从listen()
、send()
、recv()
都是阻塞IO.
阻塞型接口:
- 系统调用(一般是IO接口);
- 不返回调用结果;
- 让当前线程一直阻塞;
- 只有当该系统调用获得结果或者超时出错时才返回.
几乎所有的IO接口(包括socket接口)都是阻塞型的。这给网络编程带来了一个很大的问题,如在调用recv(1024)
的同时,线程将被阻塞,在此期间,线程将无法执行任何运算或响应任何的网络请求。
解决办法解析:
- 服务端开启多进程或者多线程,与客户端通讯,任何一个线程或进程阻塞都不会影响其他线程或进程;
- 无限制地开启多进程或多线程增加了服务器压力, 所以用到线程池或进程池, 限制服务器运行的线程后进程数量;
- “线程池”和“连接池"技术也只是在一定程度上缓解了频繁调用IO接口带来的资源占用。而且,所谓“池”始终有其上限,当请求大大超过上限时,“池”构成的系统对外界的响应并不比没有池的时候效果好多少。所以使用“池”必须考虑其面临的响应规模,并根据响应规模调整“池”的大小。
- 对应上例中的所面临的可能同时出现的上千甚至上万次的客户端请求,“线程池”或“连接池”或许可以缓解部分压力,但是不能解决所有问题。总之,多线程模型可以方便高效的解决小规模的服务请求,但面对大规模的服务请求,多线程模型也会遇到瓶颈,可以用非阻塞接口来尝试解决这个问题。
非阻塞IO
- 非阻塞的
recvform
系统调用之后,进程并没有被阻塞,内核马上返回给进程,如果数据还没准备好,此时会返回一个error
; - 进程在返回之后,可以干点别的事情,然后再发起
recvform
系统调用。重复上面的过程,循环往复的进行recvform
系统调用。这个过程通常被称之为轮询; - 轮询检查操作系统内核数据,直到数据准备好,再拷贝数据到进程,进行数据处理.
在非阻塞式IO中,用户进程其实是需要不断的主动询问kernel数据准备好了没有。
#服务端
from socket import *
server = socket(AF_INET, SOCK_STREAM)
server.bind(('127.0.0.1',8099))
server.listen(5)
server.setblocking(False)
rlist=[]
wlist=[]
while True:
try:
conn, addr = server.accept()
rlist.append(conn)
print(rlist)
except BlockingIOError:
del_rlist=[]
for sock in rlist:
try:
data=sock.recv(1024)
if not data:
del_rlist.append(sock)
wlist.append((sock,data.upper()))
except BlockingIOError:
continue
except Exception:
sock.close()
del_rlist.append(sock)
del_wlist=[]
for item in wlist:
try:
sock = item[0]
data = item[1]
sock.send(data)
del_wlist.append(item)
except BlockingIOError:
pass
for item in del_wlist:
wlist.remove(item)
for sock in del_rlist:
rlist.remove(sock)
server.close()
#客户端
from socket import *
c=socket(AF_INET,SOCK_STREAM)
c.connect(('127.0.0.1',8080))
while True:
msg=input('>>: ')
if not msg:continue
c.send(msg.encode('utf-8'))
data=c.recv(1024)
print(data.decode('utf-8'))
非阻塞IO模型绝不被推荐:
- 循环调用
recv()
和accept()
将大幅度推高CPU占用率,而且大部分时间都在做无用功,在低配主机下极容易出现卡机情况; - 响应不及时, 若中途有客户端连接的话,服务端程序也不能及时切换到
accept()
进行连接;
多路复用
利用select监听多个接口accept,recv,send,单线程下实现监听多个客户端,但是系统不会通知,只能每次循环遍历发现。
与非阻塞IO区别:
- 传统方法是每进来一个IO流会分配一个新的进程管理;
- 多路复用是在单个线程中通过记录每个IO流的状态来同时管理多个IO流,相当于机场管理系统,记录通过记录每架飞机的状态来进行调度管理.
举例:
1、select/poll:饭店服务员(内核)告诉饭店老板(用户程序):"现在有客人结账"但是这个服务员没人明确告诉老板,哪几桌的客人结帐。老板得自儿一个一个桌子去问。
2、epoll:饭店服务员(内核)告诉饭店老板(用户程序):"1,2,5号客人结账"老板就可以直接去1,2,5号桌收钱了。
# 服务端,可以实现同时服务多个客户端
from socket import *
import select
server = socket(AF_INET, SOCK_STREAM)
server.bind(('127.0.0.1', 8800))
server.listen(5)
server.setblocking(False)
print('等待连接.....')
r_list = [server, ] # 接收接口列表,accept和recv
w_list = [] # 读取接口列表
w_data = {}
while True:
rl, wl, el = select.select(r_list, w_list, [], 1)
# 调用select方法,开始监听socket, 主要是accept连接状态, conn的接收和发送数据
# rl,wl为询问后返回的结果
print(rl)
print(wl)
print(r_list)
# 一旦rl有返回结果,说明接收到数据,开始执行
for sock in rl:
if sock == server:
conn, addr = sock.accept()
r_list.append(conn) # 将读取数据就绪状态的conn放入rl列表中
else:
# 处理recv阻塞, 更新wl列表,rl列表和wlist字典
try:
data = sock.recv(1024) # 当前conn接收到数据
if not data:
# 客户端连接断开, 首先关闭服务端的管道
sock.close()
# 从接收数据列表中删除conn对象
r_list.remove(sock)
continue
w_list.append(sock) # 将发送数据就绪的conn放入wl列表中
w_data[sock] = data.upper()
except Exception:
sock.close()
r_list.remove(sock)
# 处理wl列表,发送数据,更新wl列表和wdata字典
for sock in wl:
sock.send(w_data[sock])
w_list.remove(sock)
w_data.pop(sock)
# 客户端
from socket import *
client = socket(AF_INET, SOCK_STREAM)
client.connect(('127.0.0.1', 8800))
while True:
msg = input('>>> ').strip()
if not msg: continue
client.send(msg.encode('utf-8'))
data = client.recv(1024)
print(data.decode('utf-8'))
异步IO
类似与多路复用,但是不同的是当数据准备好后系统会发送通知信号.