zoukankan      html  css  js  c++  java
  • I/O模型

    一、I/O模型前戏

      说起I/O模型,那么就需要先说说什么是I/O操作了,I/O是Input/Ouput输入、输出的意思,有的时候需要网络和其他应用程序或者服务进行数据交换,这时候就要用到网络IO。但是我们知道服务端与客户端之间的数据是需要经过网卡这种硬件的,应用程序是无法操作硬件的,此时操作系统就做为中间的桥梁了。

    上述就是j就是客户端发送数据服务端就行接收的过程:

    • 客户端发送的数据必须先放到其内核态的缓冲区中
    • 服务端操作系统通过网卡将客户端内核态缓冲区数据发送到服务端内核态数据缓冲区
    • 客户端操作系统将内核态缓冲区数据copy到用户态,完成数据的接收

    二、阻塞I/O与非阻塞I/O

    (一)背景

      在之前的socket通信中,服务端通过两层循环实现多连接与通信,但是当一个客户端与服务端进行通信时,其它客户端必须排队等候,只有当通信客户端断开其它客户端才能一个一个进行通信,那么在这排队等候这段时间就是服务端阻塞。

    import socket
    
    #创建socket对象
    sock_ser = socket.socket(socket.AF_INET,socket.SOCK_STREAM)
    
    #绑定ip和端口
    sock_ser.bind(('127.0.0.1',8000))
    
    #监听ip和端口
    sock_ser.listen(5)
    
    # 不断循环连接,等待多个客户端连接
    while True:
        conn, addr = sock_ser.accept()
        #不断循环通信,与客户端连续交互
        while True:
            #加入异常处理,如果有客户端断开,服务端不会报错,继续处理下一个连接的请求
            try:
                msg = conn.recv(1024) #收消息
                if msg:
                    print('客户端发来的消息是:', msg.decode()) #客户端发来的消息是: b'xe4xbdxa0xe5xa5xbd'
                    # 发送消息
                    conn.send(msg.upper())
                else:
                    break #如果没有消息发送过来就退出循环
            except Exception as e:
                break
    #关闭连接
        conn.close()
    #关闭套接字socket
    sock_ser.close()
    
    sock_ser处理客户端断开异常
    server
    import socket
    
    #创建socket对象
    sock_cli = socket.socket(socket.AF_INET,socket.SOCK_STREAM)
    
    #连接服务端监听的ip和端口
    sock_cli.connect(("127.0.0.1",8000))
    
    while True:
        #发送消息
        msg = input('>>:')
        if msg:
            sock_cli.send(msg.encode()) #将字符串转成字节,或者sock_cli.send(bytes(msg,encoding=""utf-8))
            #接收消息
            data = sock_cli.recv(1024)
            print('服务端发来的消息',data.decode()) #服务端发来的消息 b'xe4xbdxa0xe5xa5xbd'
        else:
            break
    #关闭套接字socket对象
    sock_cli.close()
    
    复制代码
    client

    (二)阻塞I/O模型

    像上面的socket操作就是阻塞I/O模型,上面的socket中设计到server端的accept函数以及接受数据的recv函数都是阻塞函数。

      当server端接收数据时,会进行系统调用,因为数据还处在系统的内核态,只有通过操作系统才能进行数据的读操作,系统的内核态此时会查看数据是否已经从客户端的内核态接收过来,如果没有就会等待;此时server端就会陷入到与一个客户端的通信循环中,一直处于阻塞状态,当系统内核发现数据已经接收过来后就会将数据从内核态拷贝到用户态,此时就接收到消息了。

      值得注意的是上图中等待接收数据是阻塞状态以及等待拷贝数据的过程还是处于阻塞的状态。

      阻塞I/O的劣势也就显现出来了,当遇到I阻塞时,只能等待(即使阻塞时cpu是空闲的情况下)。

    (三)非阻塞I/O模型

      在上述的阻塞I/O模型中,socket服务端等待接收客户端数据,此时可以通过设置socket参数使其变为non-blocking,那么不会一直等待内核态获取数据,但是一旦内核态获取到数据并且从内核态拷贝到用户态时还是需要等待的,因此所谓的非阻塞也不是完全非阻塞的,它的原理如上图所示。

      当server获取内核态的数据时,会发起系统调用,系统会看看内核态是否有数据,如果没有就会返回一个错误(它不会在那一直等待,而是立即返回),此时serve会获取到数据,并且可以执行其他操作;过一会再发起系统调用,看看是否有数据,过多长时间是由你自己来控制,直到有数据。

    #创建socket对象
    sock_ser = socket.socket(socket.AF_INET,socket.SOCK_STREAM)
    
    #绑定ip和端口
    sock_ser.bind(('127.0.0.1',8000))
    
    #非阻塞操作
    sock_ser.setblocking(False)
    
    #监听ip和端口
    sock_ser.listen(5)
    
    #不断循环连接,等待多个客户端连接
    
    while True:
        #异常处理是因为没有阻塞,一启动如果没有数据就会返回错误,所以进行异常处理
        try:
            conn, addr = sock_ser.accept() #不再阻塞
            #通信,与客户端交互
            msg = conn.recv(1024) #收消息
            if msg:
                print('客户端发来的消息是:', msg.decode()) 
                # 发送消息
                conn.send(msg.upper())
            # 关闭连接
            conn.close()
            # 关闭套接字socket
            sock_ser.close()
        except Exception as e:
            time.sleep(2) #多长时间再进行系统调用查看数据是否已经到内核态了

    这种非阻塞方式虽然解决了在阻塞过程中cpu可以处理其它事情而不必等待的问题,但是它也是有其不利的地方。

    • 多次进行系统调用
    • 如果在两次系统调用之间数据已经到了,但是它此时还不能接受数据,必须等到进行系统调用时才可获取数据

    三、I/O多路复用模型

    I/O多路复用模型比起上面的模型,最大的优势就是它可以实现并发效果,实现并发效果一般有以下几种情况:

    • 每收到一个请求,创建一个新的进程,来处理该请求;
    • 每收到一个请求,创建一个新的线程,来处理该请求;
    • 通过I/O多路复用来实现;

      I/O多路复用(又被称为“事件驱动”)中复用的就是线程,I/O多路复用是基于单线程的,在一个线程内并发交替地顺序完成(本质就是多协程),它使用到select、poll或者epoll等这样的模块,会监视所有的socket对象,当某个socket有变化时,那就是有消息传送过来,server端就可以进行接收数据并且配合非阻塞I/O模型使用。

    (一)事件驱动模型

     当你使用鼠标在页面上点击一个a标签后会跳转到新的页面,那么这里就可以使用事件驱动模型来解释。

    • 鼠标就是一个产生事件的事件源
    • 使用鼠标点击就是一个事件
    • 跳转到新的页面就是点击过后做出的反应,也可以称为鼠标点击事件调用回调函数产生的效果

    在事件驱动模型中很重要的一点是,如何判断是否产生了事件源(鼠标是否点击了),在事件驱动模型中是这样做的:

    (1)建立一个事件队列

    (2)如果事件源(鼠标)产生事件了,就往事件队列中添加一个事件

    (3)有个循环不断的从事件队列中取出事件,执行对应事件的回调函数

    注意:事件驱动的监听事件是由操作系统调用的cpu来完成的

    (二)I/O多路复用模型

      I/O多路复用模型和阻塞I/O模型差不多,也是在等待内核态获取数据和拷贝数据到用户态两处进行阻塞,并且还多一次系统调用,但是它使用了select模块,就可以进行并发,这使得它的优势立即显现出来并且它就是基于事件驱动模型来完成的。

    import select
    import socket
    
    sk = socket.socket(socket.AF_INET,socket.SOCK_STREAM)
    sk.bind(('127.0.0.1',8000))
    sk.listen(5)
    #非阻塞
    sk.setblocking(False)
    #一开始将sk加入rlist进行监视
    rlist = [sk,] 
    
    while True:
        #监视rlist中的sk,conn是否发生变化,如果发生变化就会将其添加到rlist
        r,w,e = select.select(rlist,[],[],5)
        #循环rlist
        for i in rlist:
            print('i',i)
            #判断如果是sk,证明有客户端连接,将其添加到rlist中进行监视
            if i == sk:
                #通过accept获取conn
                conn,addr = i.accept()
                #设置为非阻塞
                conn.setblocking(False)
                #将其添加到rlist进行监视
                rlist.append(conn)
            #如果是conn发生变化就说明是与某一个conn进行通信
            else:
                data = i.recv(1024)
                print("服务端收到的数据是",data.decode())
                i.sendall(data.upper())
                i.close()

      在上面代码中,rlist是一个事件列表,sk当作一个事件被加入到事件列表中,当sk有变化时(有客户端连接时)就会执行对应函数accept,获取客户端连接conn。并且把conn又当作一个事件添加到事件列表rlist,重复监视。

      select模块监视的的sk和conn什么情况下才叫做有变化呢?这涉及select的触发机制,它是水平触发。

      水平触发就是一直处于一个电信号的持续状态下,计算机只能识1,0这样的高低位信号。

      值得注意的是:

    • 如果内核缓冲区没有数据--->等待--->数据到了内核缓冲区,转到用户进程缓冲区;
    • select监听的是某个socket对应的内核,如果内核缓冲区有了数据,再调用accept或recv时,直接将数据转到用户缓冲区。

    (三)selectors模块

    selectors模块就是在select模块上的一次封装,使用起更加简单:

    import selectors
    import socket
    
    #根据操作系统选择合适的select
    sel = selectors.DefaultSelector()
    
    #变化的是sock对象,执行此方法,进行连接
    def accept(sock, mask):
        #接收client端的conn以及addr
        conn, addr = sock.accept()  # Should be ready
        print('accepted', conn, 'from', addr)
        #将其设置为非阻塞
        conn.setblocking(False)
        #将conn与read进行绑定并且监听conn对象
        sel.register(conn, selectors.EVENT_READ, read)
    
    #变化的对象是conn,执行此方法,进行通信
    def read(conn, mask):
        #对方断开连接,进行异常处理防止报错
        try:
            data = conn.recv(1000)  # Should be ready
            #如果发过来的是空数据,直接走异常处理
            if not data:
                raise Exception
            #否则接收数据并回复
            print('echoing', repr(data), 'to', conn)
            conn.send(data)  # Hope it won't block
        except Exception as e:
            print('closing', conn)
            #从一个监听集合中去掉监听的conn对象
            sel.unregister(conn)
            conn.close()
    
    sock = socket.socket()
    sock.bind(('localhost', 8080))
    sock.listen(100)
    sock.setblocking(False)
    #将sock对象与accept方法进行绑定,并且监听socket对象
    sel.register(sock, selectors.EVENT_READ, accept)
    
    while True:
        #监听所有的socket对象,类似select中的rlist,[sock,conn,...]
        events = sel.select()
        #循环变换的socket对象,其中的callback就是socket对象绑定的方法(通过register进行绑定)
        for key, mask in events:
            #如果是sock变化,callback就是绑定的accept方法
            callback = key.data
            #执行accept方法,传入变化的socket对象,就是sock,mask是一个整数
            callback(key.fileobj, mask)

      selectors模块中使用到了register和unregister函数,register的作用实际上就是将所有需要监听的文件描述符(socket对象)对象的值加入到集合中进行监听以及绑定该描述符所对应的回调函数。

     def register(self, fileobj, events, data=None):
            key = super().register(fileobj, events, data)
            if events & EVENT_READ:
                self._readers.add(key.fd) # self._readers = set()
            if events & EVENT_WRITE:
                self._writers.add(key.fd) #self._writers = set()
            return key

    在register中就是将已经断开client的文件描述符对象conn进行从监视集合中去除。

     def unregister(self, fileobj):
            key = super().unregister(fileobj)
            #Remove an element from a set if it is a member
            self._readers.discard(key.fd)
            self._writers.discard(key.fd)
            return key

    当不需要监听某一个文件对象时,使用unregister注销它.这会使得它从_readers和_writers中被弹出.

    四、异步I/O模型

      异步I/O模型就是在发起系统调用获取数据的过程中,用户态没有一丝阻塞的状态,内核态的数据可能还在等待接收,但是在用户态的层面上接着做其它的事情,cpu没有闲置,当内核态数据已经有了的时候,会拷贝到用户态,然后操作系统会给一个信号数据已经接收完成。

    与之相对的就是同步I/O模型,那就是在获取数据过程中会有阻塞状态,上面说的阻塞I/O模型、非阻塞I/O模型、I/O多路复用模型都是同步I/O模型,因为它们都有阻塞状态存在。

    • 阻塞I/O模型全程阻塞(等待内核获取数据、等待从内核态拷贝数据到用户态)
    • 非阻塞I/O模型第二阶段阻塞(等待从内核态拷贝数据到用户态)
    • I/O多路复用模型全程阻塞(等待内核获取数据、等待从内核态拷贝数据到用户态)

    参考:

    https://docs.python.org/3.6/library/selectors.html

    https://docs.python.org/3.6/library/select.html

    https://www.cnblogs.com/yuanchenqi/articles/5722574.html

    当我们不需要监听某一个文件对象时,使用unregister注销它.这会使得它从_readers_writers中被弹出.
    作者:回首忆惘然
    链接:https://www.imooc.com/article/48203
    来源:慕课网
    当我们不需要监听某一个文件对象时,使用unregister注销它.这会使得它从_readers_writers中被弹出.
    作者:回首忆惘然
    链接:https://www.imooc.com/article/48203
    来源:慕课网
    当我们不需要监听某一个文件对象时,使用unregister注销它.这会使得它从_readers_writers中被弹出.
    作者:回首忆惘然
    链接:https://www.imooc.com/article/48203
    来源:慕课网
  • 相关阅读:
    python计算机视觉项目实践
    Codeforces Round #256 (Div. 2) B (448B) Suffix Structures
    SonarLint插件的安装与使用
    后缀表达式求值
    有用代码段2
    提高Java代码质量的Eclipse插件之Checkstyle的使用具体解释
    Intellij Idea搭建Spark开发环境
    代码备忘, TODO宏实现
    浏览器自己主动填表安全漏洞:查看浏览器保存的password
    PDO 查询mysql返回字段整型变为String型解决方法
  • 原文地址:https://www.cnblogs.com/shenjianping/p/11618647.html
Copyright © 2011-2022 走看看