美文网首页初见-码农好文有约简友广场
Python(二十八)I/O多路复用

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

作者: Lonelyroots | 来源:发表于2021-12-01 17:56 被阅读0次

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

    相关文章

      网友评论

        本文标题:Python(二十八)I/O多路复用

        本文链接:https://www.haomeiwen.com/subject/gpasxrtx.html