IO模型

一. 引言

案例基于TCP的套接字通信

首先我们先写一个简单的套接字通信(仅仅只是实现一个客户端连接)

客户端

import socket

server = socket.socket(socket.AF_INET, socket.SOCK_STREAM)
server.bind(('127.0.0.1', 8888))
server.listen(5)
while True:
    conn, addr = server.accept()
    while True:
        data = conn.recv(1024)
        conn.send(data.upper())

服务端

import socket

client = socket.socket(socket.AF_INET, socket.SOCK_STREAM)
client.connect(('127.0.0.1', 8888))
while True:
    msg = input('>>')
    client.send(msg.encode('utf-8'))
    data = client.recv(1024)
    print(data)

问题: 我们会发现,在同一时间内我们只能连接一个客户端,另一个客户端一直处于阻塞状态,只有等到已经连接的客户端断开连接之后另一个客户端才能连接。也就是客户端的连接不能实现并发。那我们应该怎么去解决这样的问题呢?

解决方式一: 通过多线程或者多进程的方式将__建立连接__和__通信循环__两个任务异步执行,从而实现一种客户端并发连接的效果。以下以线程池的方式来实现此并发效果。

import socket
from concurrent.futures import ThreadPoolExecutor

server = socket.socket(socket.AF_INET, socket.SOCK_STREAM)
server.bind(('127.0.0.1', 8888))
server.listen(5)
# 建立一个线程池,默认线程数是cpu核数*5
pool = ThreadPoolExecutor()


def communication(conn):
    while True:
        data = conn.recv(1024)
        conn.send(data.upper())


while True:
    conn, addr = server.accept()
    # 此时我们并没有真正的解决I/O的操作,我们依然需要去等待I/O,
    # 只是说在等待I/O的过程中,另一个线程可以执行通信循环的任务而已
    pool.submit(communication, conn)

解决方式二: 我们可以在单线程的情况下使用协程来主动的切换不同的任务,从而达到一种并发的效果。需要调用gevent模块

import socket
from gevent import monkey, spawn
monkey.patch_all()  # 打补丁

def get_server(ip, port, backlog=5):
    """任务一建立连接"""
    server = socket.socket(socket.AF_INET, socket.SOCK_STREAM)
    server.bind((ip, port))
    server.listen(backlog)
    while True:
        print('hellostart')
        conn, _ = server.accept()
        print('hello')
        # 当执行完spawn代码之后当前程序该执行执行,只是在等待一个信号
        # 当前程序如果发生阻塞,就会去执行communication函数,而不会让cpu认为当前程序进入了阻塞状态
        spawn(communication, conn)

def communication(conn):
    """任务二通信循环"""
    print(conn)
    while True:
        data = conn.recv(1024)
        conn.send(data.upper())

if __name__ == '__main__':
    get_server('127.0.0.1', 8888, 5)

二. 网络I/O模型

两种时间状态:wait_data和copy_data

网络通信的过程:client应用程序 ---> client系统缓存 ---->server系统缓存----->server端应用程序

copy_data: 指的是应用程序将要发送的数据拷贝一份到系统缓存的时间。

wait_data: 指的是系统缓存发送到对端系统缓存的时间。(网络IO的时间大部分都在此)

wait_copy_data

阻塞I/O模型

​ 对于一个程序而言,如果遇到IO之后就会发生阻塞的现象,所以说阻塞I/O模型其实是我们最经常见到的一种模型。

​ 例如我们之前写的一个简单的tcp套接字通信,如果遇到accept,那么程序就只能等待客户端的连接,连接上之后如果遇到了recv也只能等待接收数据,不能去执行其他的操作,因此我们看到的是简单的tcp通信同一时间内只能有一个客户端与服务端进行连接与通信。

​ 为了解决上面的问题,我们运用了多线程和多进程的概念来实现一种并发的效果,但是从本质上来讲,并没有真正的解决IO阻塞的问题,只是通过另一种方式巧妙的避开了IO阻塞,程序还是要accept或者recv阻塞的,只是说在阻塞的过程中又开启了一个进程或者线程来执行其他的任务。

​ 虽然说进程或者西纳城实现了并发,但是创建进程或者线程都有一定的开销,因此我们不可能无限制的创建进程或者线程,因此引入了线程池和进程池的概念,规定了在此计算机中最大能开启的线程数或者进程数,从某种意义上来讲提高了客户端并发的效率。(如果没有线程池或者进程池的保护,当涌入的客户端较多,可能导致计算机无法正常服务)

总结:进程和线程本质上都没有解决IO阻塞的问题,只是巧妙的避开了IO阻塞而已。

下图为描述IO阻塞的状态图

​ 当程序执行了recv之后会发起一个系统调用,然后程序就在此阻塞了,一致等到有数据到达系统缓存之后从系统缓存中取出数据继续进行其他的操作。在阻塞模型中程序的效率非常第,大部分的时间都浪费在了wait_data时间上面。

​ 就像是我要去买一个包子吃,但是这个时候包子还没有做出来,我就一直在这盯着做包子的过程,什么时候做出来了我什么时候拿着吃。

io模型

非阻塞I/O模型

​ 为了解决上面效率低的问题,出现了非阻塞IO模型,也就是整个程序中没有IO阻塞了。如下图,当我的程序发起了一个系统调用之后,系统会返回一个信号告诉我有没有数据,如果没有数据,就去执行其他的操作,过一会再过来发起一个系统调用看看有没有数据,如果没有继续执行其他的操作,直到有数据了然后copy_data就可以了。

​ 就像是我又来买包子吃了,老板一看我来了,给我说:小伙子,现在还没有做出来呢。我一看,好吧,那我先去逛逛商场吧,逛了一会商场之后饿了,又回去看看包子做出来没有,如果做出来了我就直接拿起来吃了。

​ 非阻塞模型其实本质上是让程序在阻塞的过程中做其他的事情了,真正的解决了阻塞的问题。虽然说在每次系统调用的时候会花费点时间,但是相对于处理的其他任务而言显然是非常值得的。

非阻塞模型

案例: 以TCP的套接字通信为例来说一下这个非阻塞究竟是怎么实现的

所用的到的比较重要的知识点:

  1. 异常的捕捉 因为之前需要阻塞的地方都不阻塞了,如果没有数据接收肯定是要产生异常的。
  2. setblocking(False) 设置 对于套接字而言要设置此属性为False, 这样才能让阻塞变成非阻塞。

accept非阻塞的实现

import socket

server = socket.socket(socket.AF_INET, socket.SOCK_STREAM)
server.bind(('127.0.0.1', 8888))
server.listen(5)
# 设置成非阻塞模型,代表下面的所有网络IO阻塞都是非阻塞的
# 也就是之前的accept,recv,send都不在阻塞了,运行到此处无论是否收到数据都会立马执行下一行代码
server.setblocking(False)
while True:
    # 因此当执行到这一行代码的时候,发现没有客户端连接,但是又不阻塞,所以会报错
    # 此处的报错信息就是操作系统返回的一个信号,我们可以通过捕捉此信号来得知当前有没有客户端连接
    conn, addr = server.accept()

捕捉错误信息

# 用来表示当前已经拿到的连接数
r_list = []
while True:
    try:
        conn, addr = server.accept()
        # 每接收一个客户端的连接都会添加到r_list列表中
        r_list.append(conn)
    except BlockingIOError:
        # 当我捕捉到信号之后,就代表着当前系统缓存中还没有数据,因此我要做其他的事情了
        # 因此在异常代码块中我们需要做的是处理接收数据和发送数据的任务
        print('这段代码是用来执行接收数据和发送数据的任务的')

recv非阻塞的实现

处理recv阻塞和处理accept是一样的,都是需要捕捉信号

 print('这段代码是用来执行接收数据和发送数据的任务的')
        # 开始处理
        for conn in r_list:
            try:
                # 当此处没有收到数据的时候就会发生BlockingIOError类型的异常,因此我们需要捕捉
                data = conn.recv(1024)
            except BlockingIOError:
                # 当我捕捉到异常的时候只是代表连接的客户端中仅仅只有一个没有给我发送数据
                # 因此只需要continue继续去看其他的客户端有没有发送数据就可以了
                continue

__问题__1:在windows中 当一个客户端断开之后,如果正好在conn.recv的时候就会报ConnectionResetError

print('这段代码是用来执行接收数据和发送数据的任务的')
        # 这里做了小小的更改,做了一个浅拷贝,是为了防止在迭代的过程中对列表进行修改
        for conn in r_list[:]:
            try:
                data = conn.recv(1024)
            except BlockingIOError:
                continue
            except ConnectionResetError:
                # 如果此处捕捉到了异常,就代表着客户端断开连接了,因此我们需要把当前的这个客户端从r_list中清理掉
                conn.close()
                r_list.remove(conn)

问题2: 在Linux中,当一个客户端断开之后,会收到空数据,并不会报错

 print('这段代码是用来执行接收数据和发送数据的任务的')
        for conn in r_list[:]:
            try:
                data = conn.recv(1024)
                # 在linux系统中,需要在此处判断是否又客户端断开,因此我们需要在此处判断
                # 如果断开则删除掉此连接
                if not data:
                    conn.close()
                    r_list.remove(conn)
                    continue

send非阻塞的实现

send的处理和recv的处理非常相似,在此处就不在分析,代码如下

        for conn, data in w_list[:]:
            try:
                conn.send(data.upper())
            except BlockingIOError:
                continue
            except ConnectionResetError:
                # 当前连接断掉之后需要删除两个列表中关于此连接的信息
                conn.close()
                w_list.remove((conn, data))
                r_list.remove(conn)

服务端完整代码如下

import socket

server = socket.socket(socket.AF_INET, socket.SOCK_STREAM)
server.bind(('127.0.0.1', 8888))
server.listen(5)
# 设置成非阻塞模型,代表下面的所有网络IO阻塞都是非阻塞的
# 也就是之前的accept,recv,send都不在阻塞了,运行到此处无论是否收到数据都会立马执行下一行代码
server.setblocking(False)

# 用来表示当前已经拿到的连接数
r_list = []
w_list = []
while True:
    try:
        conn, addr = server.accept()
        # 每接收一个客户端的连接都会添加到r_list列表中
        r_list.append(conn)
    except BlockingIOError:
        print('当前连接的客户端数量', len(r_list))
        for conn in r_list[:]:
            try:
                # print(conn, '这是一套接字')
                data = conn.recv(1024)
                # 在linux系统中,需要在此处判断是否又客户端断开,因此我们需要在此处判断
                # 如果断开则删除掉此连接
                if not data:
                    conn.close()
                    r_list.remove(conn)
                    continue
                w_list.append((conn, data))
            except BlockingIOError:
                continue
            except ConnectionResetError:
                conn.close()
                r_list.remove(conn)
        for item, data in w_list[:]:
            try:
                item.send(data.upper())
                w_list.remove((item, data))
            except BlockingIOError:
                continue
            except ConnectionRefusedError:
                # 当前连接断掉之后需要删除两个列表中关于此连接的信息
                item.close()
                w_list.remove((item, data))
                r_list.remove(conn)

客户端创建100个线程去连接服务端

import socket
from threading import Thread,current_thread
def clients():
    client = socket.socket()
    client.connect(('127.0.0.1', 8888))
    while True:
        msg = '%s 连接了' % current_thread().name
        client.send(msg.encode('utf-8'))
        data = client.recv(1024)
        print(data.decode('utf-8'))
for i in range(100):
  Thread(target=clients).start

I/O多路复用

​ 非阻塞IO虽然说大大的提高了程序的效率,但是当没有人来连接或者没有收到数据的时候也会消耗大量的cpu资源,就像是一个病毒一样大量的消耗cpu的资源。因此,我们需要考虑一个机制可以实现我等一会去查看一下有没有数据,如果有数据,我就一块把它拿过来进行处理一下,如果没有值我就继续在这等着。

​ 这就好比我现在不仅要买包子,我还要去买芹菜,还要去买鸡蛋,但是现在东西都还没有做好,因此我现在还是要等待,一旦有做好的,我就把这些东西吃掉,也就是说当我单纯的想买包子的时候,这个性质和阻塞IO是一样的,但是如果我既要买包子又要买其他的东西的时候,就和阻塞IO不一样了。就像是多条路径一样,这些东西我全部拿到了,然后一块进行处理。

__核心:__IO多路复用本质上也是尽最大的可能利用cpu的资源,只不过不用每次不像非阻塞IO一样需要不停的询问是否有数据,因此节省了cpu的资源,但是效率相对来说也较低。

多路复用

__案例: __基于TCP的套接字通信

要用到的比较重要的知识点

select模块 select用来统一管理系统缓存中是否有数据,accept,recv,send都在管理范围之内,如果有数据,则告诉主进程进行处理

accept阻塞的处理

import socket
# 导入select模块
import select
server = socket.socket(socket.AF_INET, socket.SOCK_STREAM)
server.bind(('127.0.0.1', 9999))
server.listen(5)
server.setblocking(False)
# 包含当前已经创建的server和已经连接的客户端socket对象
r_list = [server]
# 包含的是当前已经连接的客户端对象
w_list = []
while True:
    print('客户端在等待连接')
    # 当没有客户端连接并且没有数据接收和访问的时候程序就会阻塞在这里
    rl, wl, _ = select.select(r_list, w_list, [])
    # 返回值目前能够使用到的内容为:
    # rl: 当前已经接收到连接还没有处理的socket对象以及当前连成功有数据发送过来的客户端对象
    # wl: 当前已经连接成功并且可以发送数据的客户端对象
    print('当r1和w1内有值的时候,我们就需要立马进行处理,否则会一直循环的占用cpu的资源')

​ 此时当我们运行服务端程序的时候就会发现程序阻塞在这里并且打印客户端正在连接,当有一个客户端连接之后就会一直循环的打印信息,cpu立马就会上升,这是因为当有一个客户端连接之后server就会检测到有内容,select就不会阻塞,将返回值给rl,但是此时并没有程序来处理这个值,因此会进入到一个死循环中,因此我们需要在select语句之后写一个处理rl的程序

print('当r1和w1内有值的时候,我们就需要立马进行处理,否则会一直循环的占用cpu的资源')
    for obj in rl:
        # 此时rl的值有一个server对象,因此我们需要接收里面的内容
        conn, addr = obj.accept()
        print(addr)

recv阻塞的处理

    for obj in rl:
        # 因为我们将客户端的连接也加入进了r_list中,因此我们需要判断处理obj究竟是客户端连接还是服务端连接
        if obj == server:
            conn, addr = obj.accept()
            # 当来了一个连接之后我们需要将这个客户端对象添加到r_list统一的交给select来管理
            r_list.append(conn)
            # 同时我们也应该将w_list添加到w_list中,如果需要发送数据的时候就发送数据
        else:
            # 如果对象为客户端对象,我们需要获得客户但的值
            data = obj.recv(1024)
            # 为了之后发送数据,我们需要在定义一个数据列表保存应该给哪个客户端发送哪些数据
            data_dict = {obj: data}

​ 虽然完成了recv接收数据的问题,但是当一个客户端异常断开的时候服务端也就跟着断开连接了这是不合理的,因此我们需要捕捉异常信息进行处理

try:
    data = obj.recv(1024)
    # 当收到客户端发送过来的数据之后才会将客户端连接添加进去,因此此时我们才知道应该送什么值给服务端
    w_list.append(obj)
    data_dict[obj] = data
except ConnectionResetError:
    # 当发生异常的时候,就代表客户端连接中断了,此时我们就需要将r_list中obj给关闭并且删除
    # 断开连接之后w_list其实也是要删除的,但是之后还要进行操作,因此就等到之后进行处理
    obj.close()
    r_list.remove(obj)
    # 此处我们没有必要删除w_list的原因的是当发生异常的时候obj还没有添加到w_list中,因此没有必要
    continue

send阻塞的处理

和recv很相似,不同的是当发送完数据之后就应该立刻将此对象从w_list中删除掉

 # 处理发送信号
    for obj in wl:
        try:
            obj.send(data_dict[obj].upper())
            # 当给客户端发送数据之后就应该将此连接从w_list中删除了,防止下次进来的时候循环调用
            w_list.remove(obj)
        except ConnectionResetError:
            obj.close()
            w_list.remove(obj)
            r_list.remove(obj)

服务端代码

import socket
# 导入select模块
import select
server = socket.socket(socket.AF_INET, socket.SOCK_STREAM)
server.bind(('127.0.0.1', 9999))
server.listen(5)
# 包含当前已经创建的server和已经连接的客户端socket对象
r_list = [server]
# 包含的是当前已经连接的客户端对象
w_list = []
# 用来保存应该给哪个客户端发送哪些数据
data_dict = {}
while True:
    print('客户端在等待连接')
    rl, wl, _ = select.select(r_list, w_list, [])
    print('rl和wl的值',rl, wl)
    # 处理接收信号
    for obj in rl:
        if obj == server:
            conn, addr = obj.accept()
            r_list.append(conn)
        else:
            try:
                data = obj.recv(1024)
                w_list.append(obj)
                data_dict[obj] = data
            except ConnectionResetError:
                obj.close()
                r_list.remove(obj)
                continue
    print(data_dict, '要发送的数据信息')
    # 处理发送信号
    for obj in wl:
        try:
            obj.send(data_dict[obj].upper())
            # 当给客户端发送数据之后就应该将此连接从w_list中删除了,防止下次进来的时候循环调用
            w_list.remove(obj)
        except ConnectionResetError:
            obj.close()
            w_list.remove(obj)
            r_list.remove(obj)

客户端创建100个线程去连接服务端

import socket
from threading import Thread, current_thread

def task():
    client = socket.socket(socket.AF_INET, socket.SOCK_STREAM)
    client.connect(('127.0.0.1', 9999))
    while True:
        msg = '我是%s' %current_thread().name
        client.send(msg.encode('utf-8'))
        print(client.recv(1024))

for i in range(100):
    Thread(target=task).start()

异步I/O模型

​ 异步IO是所有IO中速率最快的,因为它只需要告诉系统我需要某个值,就可以去做其他的事情了,一旦有值了之后系统会自动返回值给调用者。

​ 就像是我要去买个包子,我先告诉老板一生,我要买个包子哈,然后我就去干别的事情了,等到包子做好之后老板自动的就把包子给我送过来了,然后我就可以吃了。

异步IO

最典型的一个例子就是异步回调。

我们接下来写一个简单的异步回调

import time
from concurrent.futures import ThreadPoolExecutor
from threading import current_thread
   def task1(n):
       """这是执行的任务,将数据返回用回调函数及逆行结束"""
       print('%s' % current_thread().name)
       time.sleep(2)
       return n ** 2

   def parse(self):
       time.sleep(2)
       print('解析出来的值为%s' % self.result())

   if name == 'main':
       pool = ThreadPoolExecutor(4)
       start = time.time()
       for i in range(4):
           # 每一个线程该执行的执行,当任务task1执行完毕返回一个值之后
           # 会自动的调用parse进行数据的解析,效率很高
           pool.submit(task1, i).add_done_callback(parse)
       pool.shutdown()
       print(time.time() - start)


posted @ 2019-04-12 16:13  沉沦的罚  阅读(110)  评论(0编辑  收藏  举报