非阻塞套接字与IO多路复用
一、基本IO模型
1、数据流的概念
数据流(data stream)
是一组有序,有起点和终点的字节的数据序列,是只能被读取一次或少数几次的点的有序序列。其包括输入流和输出流。
数据流分为输入流(InputStream)
和输出流(OutputStream)
两类。输入流只能读不能写,而输出流只能写不能读。通常程序中使用输入流读出数据,输出流写入数据,就好像数据流入到程序并从程序中流出。采用数据流使程序的输入输出操作独立于相关设备。
输入流可从键盘或文件中获得数据,输出流可向显示器、打印机或文件中传输数据。
2、IO解释与IO交互
IO
即input and output
。在Unix世界里,一切皆文件。而文件是什么呢?文件就是一串二进制流
,不管socket、还是FIFO、管道、终端,一切都是文件,一切都是流。在信息交换的过程中,对这些流进行数据的收发操作,简称为I/O操作(input and output)
往流中读出数据,系统调用read。写入数据,系统调用write。但是计算机里有这么多的流,怎么知道要操作哪个流呢?做到这个的就是文件描述符
,即通常所说的fd
。一个fd就是一个整数,所以对这个整数的操作,就是对这个文件(流)的操作。创建一个socket
,通过系统调用会返回一个文件描述符,那么剩下对socket的操作就会传化为对这个描述符的操作。
3、阻塞IO
在实际情况汇总,很多时候数据在一开始还没有到达,这个时候内核就要等待足够的数据到来。
而在用户进程这边,整个进程会被阻塞,当内核一直等到数据准备好了,它就会将数据从内核中拷贝到用户内存,然后返回结果,用户进程才解除阻塞的状况,重新运行起来。
二、非阻塞IO模型与非阻塞套接字
1、非阻塞IO模型
从用户进程角度讲,它发起一个read操作后,并不需要等待,而是马上就得到了一个结果,用户进程判断结果是一个error时,它就知道数据还没有准备好。
于是它可以再次发送read操作,一但内核中的数据准备好了,并且又再次收到了用户进程的系统调用,那么它马上就将数据拷贝到了用户内存,然后返回,非阻塞的接口相比于阻塞模型接口的显著差异在于,在被调用之后立即返回
2、非阻塞IO
import socket
server = socket.socket()
server.setblocking(False) #设置为非阻塞套接字,且需要在其他的执行之前
server.bind(('127.0.0.1',8989))
server.listen(6)
while True:
try:
conn,addr = server.accept()
data = conn.recv(1024)
conn.send(data)
except BlockingIOError:
pass
except Exception:
pass
利用异常处理来处理非阻塞IO产生的异常
3、并发与并行
3-1、并发
指一个时间段中有几个程序都处于已启动运行到运行完毕之间,且这几个程序都是在同一个处理机上运行,但任一个时刻点上只有一个程序在处理机上运行
3-2、并行
指一个时间段中有几个程序都处于已启动运行到运行完毕之间,且这几个程序都是在同一个处理机上运行,并任一个时刻点上有多个程序在处理机上运行。
4、实现并发
import socket
server = socket.socket()
# server.setblocking(False) #设置为非阻塞套接字,且需要在其他的执行之前
server.bind(('127.0.0.1',8989))
server.listen(6)
all_conn = [] # 添加一个列表来存储生成的对等连接套接字
while True: # 第一层循环只是用来生成对等连接套接字
try:
conn,addr = server.accept()
server.setblocking(False)
data = conn.recv(1024)
print(data)
conn.send(data)
except BlockingIOError:
pass
except Exception as e:
print(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(e)
并发
代码实现很简单,但是要理解过程
主要是用循环思想去解决问题
四、IO多路复用
1、IO多路复用
IO多路复用
在之前的非阻塞IO模型中,通过不断的询问来查看是否有数据,这会造成资源的浪费
。
将查看的过程由主动地查询,变成交给复用器
来完成,这样能够更加节省系统资源
,并且性能更好
。
2、epoll
非阻塞套接字与多路复用
非阻塞套接字需要自身遍历每个对等连接套接字,并且每次都是运行的IO操作
复用器不需要进行大量的IO操作,复用器会告诉你哪个对等套接字有数据过来。然后再去处理即可。epoll
epoll是一个惰性事件回调,即回调过程是用户自己去调用的,操作系统只起到了通知的作用。
epoll是linux上最好的IO多路复用器,但是也只有在linux上有,在其他的地方没有
3、代码实现
import socket
import selectors # IO多路复用选择器模块
epoll_select = selectors.EpollSelector() # 实例化
# epoll_select = selectors.DefaultSelector() # 默认选择器,根据不同操作系统自动选择
server = socket.socket()
server.bind(('127.0.0.1',8989))
server.listen(100)
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,addr = server.accept() # 生成对等连接套接字
epoll_select.register(conn,selectors.EVENT_READ,f_recv)
epoll_select.register(server,selectors.EVENT_READ,f_accept)
# selectors.EVENT_READ 有事件发生的时候,调用回调函数
while True:
events = epoll_select.select() # 查询,只要有,就查出来
for key,mask in events:
func = key.data # 函数体
conn = key.fileobj # 已经注册过的套接字
func(conn)