看完本篇文章,你将会了解到什么是I/O多路复用,以及如何通过代码来防止服务器上的阻塞现象。
1. 基本IO模型
1.1. 数据流概念
数据流(data stream)是一组有序,有起点和终点的字节的数据序列,是只能被读取一次或少数几次的点的有序序列,其包括输入流和输出流。
输入流可从键盘或文件中获得数据,输出流可向显示器、打印机或文件中传输数据。
数据流分为输入流(InputStream)和输出流(OutputStream)两类。输入流只能读不能写,而输出流只能写不能读,通常程序中使用输入流读出数据,输出流写入数据,就好像数据流入到程序并从程序中流出,采用数据流使程序的输入输出操作独立于相关设备。
1.2. IO解释与IO交互
IO即 input and output,在unix世界里,一切皆文件。而文件是什么呢?文件就是一串二进制流,不管socket、还是FIFO、管道或终端,一切都是文件,一切都是流。在信息交换的过程中,对这些流进行数据的收发操作,简称为I/O操作(input and output)。
用户空间中存放的是用户程序的代码和数据。内核空间用来存放的是内核代码和数据。
往流中读出数据,系统调用read;写入数据,系统调用write。但是计算机里有这么多的流,怎么知道要操作哪个流呢?做到这个的就是文件描述符,即通常所说的fd。一个fd就是一个整数,所以对这个整数的操作,就是对这个文件(流)的操作。创建一个socket,通过系统调用会返回一个文件描述符,那么剩下对socket的操作就会转化为对这个描述符的操作。
1.3. 阻塞IO
很多时候数据在一开始还没有到达,这个时候内核就要等待足够的数据到来。而在用户进程这边,整个进程会被阻塞,当内核一直等到数据准备好了,它就会将数据从内核中拷贝到用户内存,然后返回结果,用户进程才解除阻塞的状态,重新运行起来。
阻塞IO
2. 非阻塞IO模型与非阻塞套接字
2.1. 非阻塞IO模型
从用户进程角度讲,它发起一个read操作后,并不需要等待,而是马上就得到了一个结果,当用户进程判断结果是一个error时,它就知道数据还没有准备好。
于是它可以再次发送read操作,一旦内核中的数据准备好了,并且又再次收到了用户进程的系统调用,那么它马上就将数据拷贝到了用户内存,然后返回,非阻塞的接口相比于阻塞型接口的显著差异在于,在被调用之后立即返回。
2.2. 非阻塞IO
利用异常处理来处理非阻塞IO产生的异常
服务器代码:
import socket
server = socket.socket() # 创建一个socket对象,命名为服务器
server.setblocking(False) #设置非阻塞套接字
server.bind(('127.0.0.1',8989)) # 绑定端口,注意这里填入的是元组
server.listen(10) # 设置最大监听数,最大连接量
while True:
try:
result = server.accept() # 与客户端创建对等套接字
conn,address = result # 元组拆包
conn.setblocking(False) # 设置非阻塞io
except BlockingIOError:
pass
except Exception as e:
print(f'发生了未知异常{e}')
"""若端口被占用,可以打开虚拟机查看进程并结束它"""
# ps -aux | grep python # 查看进程
# kill -9 2821 # 这里的2821指的是服务器运行进程id,照具体情况而定
客户端代码
import socket
client = socket.socket() # 创建一个socket对象,命名为客户端
client.connect(('127.0.0.1',8989))# 连接服务器端口,注意这里填入的是元组
for i in range(10):
client.send(b'hello') # 发送hello给服务器
print(client.recv(1024))
2.3. 并发与并行
2.3.1. 并发
指一个时间段中有几个程序都处于已启动运行到运行完毕之间,且这几个程序都是在同一个处理机上运行,但任一个时刻点上只有一个程序在处理机上运行。
2.3.2. 并行
指一个时间段中有几个程序都处于已启动运行到运行完毕之间,且这几个程序都是在同个处理机上运行,并任一个时刻点上有多个程序在处理机上运行。
2.4. 实现并发
服务器代码
import socket
server = socket.socket() # 创建一个socket对象,命名为服务器
server.setblocking(False) #设置非阻塞套接字
server.bind(('127.0.0.1',8989)) # 绑定端口,注意这里填入的是元组
server.listen(10) # 设置最大监听数,最大连接量
all_conn = [] # 创建一个空列表,等待套接字添加进来
while True:
try:
result = server.accept() # 与客户端创建对等套接字
conn,address = result # 元组拆包
conn.setblocking(False) # 设置非阻塞io
all_conn.append(conn)
except BlockingIOError:
pass
except Exception as e:
print(f'发生了未知异常{e}')
for conn in all_conn: # 依次处理套接字中的数据
try:
recv_data = conn.recv(1024)
if recv_data:
print(recv_data)
conn.send(recv_data)
else:
conn.close()
all_conn.remove(conn)
except BlockingIOError:
pass
except Exception as e:
print(f'发生了未知异常{e}')
"""若端口被占用,可以打开虚拟机查看进程并结束它"""
# ps -aux | grep python # 查看进程
# kill -9 2821 # 这里的2821指的是服务器运行进程id,照具体情况而定
客户端代码
import socket
client = socket.socket() # 创建一个socket对象,命名为客户端
client.connect(('127.0.0.1',8989))# 连接服务器端口,注意这里填入的是元组
for i in range(10):
client.send(b'hello') # 发送hello给服务器
print(client.recv(1024))
3. IO多路复用
3.1. 概念介绍
在之前的非阻塞IO模型中,通过不断的询问来查看是否有数据,会造成资源的浪费。将查看的过程由主动的查询变成交给复用器完成,这样能够更加节省系统资源,并且性能更好。
3.2. epoll
3.2.1. 非阻塞套接字与多路复用
非阻塞套接宁需要自身遍历每个对等连接套接字,并且每次都进行IO操作。复用器不需要进行大量的IO操作,因为复用器会告诉哪个对等连接套接字有数据传输过来,然后再去处理即可。
3.2.2. epoll概念
epoll是一个惰性事件回调,即回调过程是用户自己去调用的,操作系统只起到通知的作用。epoll是Linux上最好的IO多路复用器,但是也只有Linux有,在其他的地方都没有。
3.2.3. 代码实现
服务器代码
import time
import socket
import selectors # 导入IO多路复用选择器
epoll_sel = selectors.EpollSelector() # 实例化一个复用器对象
default_sel = selectors.DefaultSelector() # 使用默认选择器,根据系统自动选择
server = socket.socket() # 创建一个socket对象,命名为服务器
server.bind(('127.0.0.1',8989)) # 绑定端口,注意这里填入的是元组
server.listen(10) # 设置最大监听数,最大连接量
def f_recv(conn):
recv_data = conn.recv(1024)
if recv_data:
print(recv_data)
conn.send(recv_data)
else:
conn.close()
def f_accept(server):
conn,address = server.accept()
epoll_sel.register(conn,selectors.EVENT_READ,f_recv)
# 参数一:可能会发生事件的对象;参数二:检查是否发生了事件;参数三:发生事件之后需要执行的功能。
epoll_sel.register(server,selectors.EVENT_READ,f_accept)
while True:
events=epoll_sel.select() # 是否有事件发生
time.sleep(1)
print(events)
for key,i in events: # i用来接收1
obj = key.fileobj # 注册进来的发生事件对象,采用实例对象.属性的方式进行调用
func = key.data # 要执行的方法
func(obj)
客户端代码
import socket
client = socket.socket() # 创建一个socket对象,命名为客户端
client.connect(('127.0.0.1',8989))# 连接服务器端口,注意这里填入的是元组
for i in range(10):
client.send(b'hello') # 发送hello给服务器
print(client.recv(1024))
文章到这里就结束了!希望大家能多多支持Python(系列)!六个月带大家学会Python,私聊我,可以问关于本文章的问题!以后每天都会发布新的文章,喜欢的点点关注!一个陪伴你学习Python的新青年!不管多忙都会更新下去,一起加油!
Editor:Lonelyroots
网友评论