Day37 of learning python -- IO模型

1.IO模型

一共有5种不同的IO模型:

    * blocking IO           阻塞IO
    * nonblocking IO      非阻塞IO
    * IO multiplexing      IO多路复用
    * signal driven IO     信号驱动IO
    * asynchronous IO    异步IO

分类的根据:一个是:调用这个IO的process(or thread),另一个是:系统内核(kernel)。当一个read操作发生时,该操作会经历两个阶段:

#1)等待数据准备 (Waiting for the data to be ready)
#2)将数据从内核拷贝到进程中(Copying the data from the kernel to the process)

2.阻塞IO(blocking IO)

阻塞的部分:kernel准备数据,需要等待足够的数据到来才行,然后就一直在阻塞,直到数据已经准备好了,而这时用户的进程也被阻塞了。

      当数据准备好了,kernel需要拷贝数据到用户的内存,然后kernel返回结果,用户进程才解除block的状态,重新运作起来。

3.非阻塞IO(non-blocking IO)

  从上图可以得到,当用户进程发出read操作时,如果kernel的数据没有准备好,那么它并不会block用户进程,而是立即返回一个error。利用这个error,用户可以处理这个error,转而去做别的操作,从而再次给kernel发送read操作。一旦kernel中的数据准备好了,并且又再次受到用户进程的system call,那么它马上就将数据拷贝到用户的内容,但是这个过程还是阻塞的。

  因此,在非阻塞式IO中,用户进程其实是需要不断的主动询问kernel的数据是否准备好了。

import socket
sk = socket.socket()
sk.bind(('127.0.0.1',9090))
sk.setblocking(False)    # 把socket当中所有需要阻塞的方法都改变成非阻塞
sk.listen()
conn_l = []  #用来存储所有来请求server端的conn连接
del_conn = [] # 用来存储所有已经断开与server端连接的conn
while 1:
    try:
        conn,addr = sk.accept()  # 不阻塞,但是没人连我会报错
        print('建立连接了',addr)
        # msg = conn.recv(1024)    # 1.如果不及时给我发信息,那么也会报错。2.要等待接收信息,别人不能再进来连。3.所以不能放在这个地方
        # print(msg)
        conn_l.append(conn)
    except BlockingIOError:
        # print(conn_l)
        for con in conn_l:  # conn_l为空,不会走下面的代码
            try:
                msg = con.recv(1024)  # 非阻塞,如果没有数据就会报错
                if msg == b'':
                    del_conn.append(con)
                    continue   # 跳出循环,添加下一个断开连接的conn
                print(msg)
                con.send(b'byebye')
            except BlockingIOError:pass
        for con in del_conn:
            con.close()
            conn_l.remove(con)
        del_conn.clear()    # 在循环里面,删除不了conn_l的内容,因为这个是重新赋值的迭代器的内容,不是指向同一内容的。



# 由于while 很耗内存,就有了多路复用这个方法了。
import socket
import time
import threading
def func():
    sk = socket.socket()
    sk.connect(('127.0.0.1',9090))
    sk.send(b'hello')
    time.sleep(1)
    print(sk.recv(1024))
    # ret =sk.recv(1024).decode('utf-8')
    # print(ret)
    # msg = input('>>>').encode('utf-8')
    # sk.send(msg)
    sk.close()

for i in range(20):
    threading.Thread(target=func).start()  # 起20个线程

缺点:

1).循环调用accept()/recv()将大幅度提高CPU的占用率。2).任务完成的响应延迟增大了,因为每过一段时间才去轮询read操作,而kernel可能在轮询这段时间,已经受到了消息。这会导致整体数据吞吐量的降低。

4.IO多路复用

IO多路复用的好处就是:select/epoll在于单个process就可以同事处理多个网络连接的IO,基本的原理就是select/epoll这个function会不断的轮询所负责的所有socket,当某个socket有数据到达了,就通知用户进程。

 

import select
import socket

sk = socket.socket()
sk.bind(('127.0.0.1',8080))
sk.setblocking(False)
sk.listen()

read_lst = [sk]    # 多个连接以列表的形式组织起来的
print([sk])
while 1: # [sk,conn]
    r_lst,w_lst,x_lst = select.select(read_lst,[],[])  # 此处阻塞,直到kernel检测到select负责的socket有信息过来,不论是新的连接过来了,还是已经连接的conn发送过来的数据
    print('***',r_lst)   #r_lst放回的是接收到新的连接或者有新数据来的连接
    for i in r_lst:   # 从代理反馈回来,到本程序连接
        print("代理已经接收到了数据")
        if i is sk:
            print("执行添加操作")
            conn,addr = i.accept()
            read_lst.append(conn)
        else:
            print("执行接收数据操作")
            ret = i.recv(1024)   # 此处的i相当于conn
            if ret == b'':     # 客户端断开连接时,会发送空的字符串
                read_lst.remove(i)
                continue
            # print(ret)
            i.send(b'goodbye')

# 当客户端断开连接的时候,会发空的消息给服务端。
# 操作系统帮你循环这个列表,查看每一项是否有可读的事件
import socket
import time
import threading
def func():
    sk = socket.socket()
    sk.connect(('127.0.0.1',8080))
    sk.send(b'hello')
    time.sleep(5)
    print(sk.recv(1024))
    sk.close()

for i in range(20):
    threading.Thread(target=func).start()  # 起20个线程

 select监听fd变化的过程分析:

#用户进程创建socket对象,拷贝监听的fd到内核空间,每一个fd会对应一张系统文件表,内核空间的fd响应到数据后,就会发送信号给用户进程数据已到;
#用户进程再发送系统调用,比如(accept)将内核空间的数据copy到用户空间,同时作为接受数据端内核空间的数据清除,这样重新监听时fd再有新的数据又可以响应到了(发送端因为基于TCP协议所以需要收到应答后才会清除)。

 优点:

#相比其他模型,使用select() 的事件驱动模型只用单线程(进程)执行,占用资源少,不消耗太多 CPU,同时能够为多客户端提供服务。如果试图建立一个简单的事件驱动的服务器程序,这个模型有一定的参考价值。

 缺点:

#首先select()接口并不是实现“事件驱动”的最好选择。因为当需要探测的句柄值较大时,select()接口本身需要消耗大量时间去轮询各个句柄。
#其次,该模型将事件探测和事件响应夹杂在一起,一旦事件响应的执行体庞大,则对整个模型是灾难性的。

 5.异步IO(Asynchrounous I/O)

 

  用户进程发起read操作之后,立刻就可以开始去做其它的事。而另一方面,从kernel的角度,当它受到一个asynchronous read之后,首先它会立刻返回,所以不会对用户进程产生任何block。然后,kernel会等待数据准备完成,然后将数据拷贝到用户内存,当这一切都完成之后,kernel会给用户进程发送一个signal,告诉它read操作完成了。

  进程不会发生阻塞。

总结:blocking IO,non-blocking IO,IO multiplexing都属于synchronous IO这一类。而 asynchronous I/O一个类 。根据是否有阻塞,包括数据等待或者kernel等待的拷贝数据。

  因为non-blocking IO在执行recvfrom这个system call的时候,如果kernel的数据没有准备好,这时候不会block进程。但是,当kernel中数据准备好的时候,recvfrom会将数据从kernel拷贝到用户内存中,这个时候进程是被block了,在这段时间内,进程是被block的。

 

6.Selector

  Selector模块支持不同的平台使用IO多路复用

#服务端
from socket import *
import selectors

sel=selectors.DefaultSelector()
def accept(server_fileobj,mask):
    conn,addr=server_fileobj.accept()
    sel.register(conn,selectors.EVENT_READ,read)

def read(conn,mask):
    try:
        data=conn.recv(1024)
        if not data:
            print('closing',conn)
            sel.unregister(conn)
            conn.close()
            return
        conn.send(data.upper()+b'_SB')
    except Exception:
        print('closing', conn)
        sel.unregister(conn)
        conn.close()



server_fileobj=socket(AF_INET,SOCK_STREAM)
server_fileobj.setsockopt(SOL_SOCKET,SO_REUSEADDR,1)
server_fileobj.bind(('127.0.0.1',8088))
server_fileobj.listen(5)
server_fileobj.setblocking(False) #设置socket的接口为非阻塞
sel.register(server_fileobj,selectors.EVENT_READ,accept) #相当于网select的读列表里append了一个文件句柄server_fileobj,并且绑定了一个回调函数accept

while True:
    events=sel.select() #检测所有的fileobj,是否有完成wait data的
    for sel_obj,mask in events:
        callback=sel_obj.data #callback=accpet
        callback(sel_obj.fileobj,mask) #accpet(server_fileobj,1)

#客户端
from socket import *
c=socket(AF_INET,SOCK_STREAM)
c.connect(('127.0.0.1',8088))

while True:
    msg=input('>>: ')
    if not msg:continue
    c.send(msg.encode('utf-8'))
    data=c.recv(1024)
    print(data.decode('utf-8'))

基于selectors模块实现聊天
posted on 2019-01-11 20:38  smile大豆芽  阅读(163)  评论(0)    收藏  举报