初见-码农好文有约简友广场

Python(二十八)I/O多路复用

2021-12-01  本文已影响0人  Lonelyroots

看完本篇文章,你将会了解到什么是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

上一篇 下一篇

猜你喜欢

热点阅读