IO模型

IO模型

  模型就是解决某个问题的套路,IO模型主要是用来解决IO问题的

  IO问题:输入输出的问题

  例如我需要一个用户名来执行登录操作,问题是用户名需要输入,输入需要耗时,如果输入没有完成,

  后续的逻辑代码就无法继续执行,这种默认的处理方式就是阻塞IO模型

  网络IO中必经的两个阶段

    1、wait_data:等待数据从对方发到系统的缓存区

    2、copy_data:将数据从系统缓存区拷贝到应用的内存中

阻塞IO

  阻塞IO模型指的是当遇到IO操作时,就将当前的线程阻塞住,cpu切换到其他线程执行,

  一直等到IO操作结束,拿到想要的结果时,再唤醒刚刚的线程,将状态调整为就绪态

  存在的问题

    当执行到recv时,如果对象并没有发送数据,程序就阻塞了,没办法执行其他任务

  解决方案

    多线程或者多进程:当客户端并发量非常大的时候,服务器可能无法开启新的线程或者

             进程,如果不对数量加以限制,服务器就崩溃了(无效率可言)

    线程池或者进程池:对最大并发量加以了限制,保证服务器正常运行,但是问题时,

             如果所有客户端都处于阻塞状态,那么这些线程也阻塞了,

             后面再想进来访问也进不来

    协程:使用一个线程处理所有的客户端,当一个客户端处于阻塞状态时,

       可以切换至其他客户端任务

非阻塞IO

  阻塞IO模型在执行 recv 和 accept 时 都需要经历wait_data

  非阻塞IO模型即 在执行recv 和 accept 时,不会阻塞,可以继续往下执行

  如何使用:

    将server的blocking设置为False即设置非阻塞

import socket
client = socket.socket()
client.connect(("127.0.0.1",1688))

while True:
    msg = input(">>:").strip()
    if not msg:continue
    if msg == 'q':break
    client.send(msg.encode('utf-8'))
    print(client.recv(1024).decode('utf-8'))
客户端

将阻塞设置为非阻塞之后需要注意的几个点

  1、accept  recv send 几个函数都有可能出现阻塞状态,所以都需要抓取异常

  2、需要不停的循环去问缓存区有没有数据,就需要把所有的客户端socket对象加到容器中,

     不断遍历recv,如果有值就接收,没值就不管

  3、回消息也应该把消息和对应的socket对象放到容器中,循环发送,如果发送成功,

     就从消息容器中删除,如果发送失败,说明现在缓存区是满的,下次循环再发

  4、客户端正常退出和异常退出都需要抓取异常,并且关闭socket对象,移除容器,

     这时候就需要注意发消息 的容器中还有没有这个socket对象的消息,如果有,

     一起删除,防止后续发出会抛异常

import socket
server = socket.socket()
server.bind(("127.0.0.1",8080))
server.listen()

server.setblocking(False)   # 把阻塞改为非阻塞

clients = []    # 创建容纳客户端socket对象的容器列表
msgs = []       # 创建一个保存要发送信息和socket对象对应的容器

while True:
    try:
        client,addr = server.accept()   # 如果没人连接过来,就会被捕获异常
        clients.append(client)
    except BlockingIOError:
        print("没人连接过来")
        # 接收消息,把消息保存起来
        for c in clients[:]:
            try:
                data = c.recv(1024)     # 没接收到消息就会捕获异常,接着往下执行
                if not data:
                    c.close()   # 如果客户端正常退出,在这里关闭客户端socket对象,并且移除列表
                    clients.remove(c)
                    continue        # 在这里关闭移除完之后要continue一下,不然还会把socket对象添加到保存消息列表中
                msgs.append((c,data))

            except BlockingIOError:
                print("没有任务需要接收")
            except ConnectionResetError:
                c.close()
                clients.remove(c)   # 其实在这关闭移除还需要注意一个点,那就是如果上面已经添加到保存列表去
                                    # 然后对面强行中断连接的话也会报异常

        # 发送消息
        for m in msgs[:]:
            try:
                c,data = m
                c.send(data.upper())    # 如果缓存区满了,就会捕获异常,接着执行
                msgs.remove(m)  # 信息发送完之后把消息移除
            except BlockingIOError:
                print("没有任务需要发送")
服务器端

多路复用IO

  在非阻塞IO模型中,我们要想实现并发,就必须一直询问操作系统缓存有没有数据或者能不能发送,

  这样是非常耗cpu的

  所以我们想了一个办法,就是让一个对象去统一检测,如果有socket对象可写或者可读,就把socket对象返出来

  然后进行处理,这里需要用到的模块就是select模块

  select模块中有一个select函数,这个函数可以实现帮我们检测列表中的socket对象是否可读或者可写

  select监听的socket对象是有上限的,默认为1024个

使用方法

import socket
client = socket.socket()
client.connect(("127.0.0.1",8080))

while True:
    msg = input("msg:").strip()
    if not msg:continue
    if msg == 'q':break
    client.send(msg.encode('utf-8'))
    print(client.recv(1024).decode('utf-8'))
客户端
import socket
import select
server = socket.socket()
server.bind(("127.0.0.1",8080))
server.listen()

rlist = [server,]  # 创建读的检测列表,server也是可读的socket对象,需要先加进来
wlist = []  # 创建写的检测列表
msgs = []

"""
select模块中select函数中的几个参数
rlist: 里面存储检测是否可读(recv、accept)的socket对象
wlist: 里面存储检测是否可写(send)的socket对象
xlist: 存储你需要关注的异常条件,忽略
timeout:设置超时时间,如果超过设定时间还没有检测到可读或者可写,就返回空列表(一般不设置)
"""
while True:
    readable_list,writeable_list,_ = select.select(rlist,wlist,[])

    # 循环可读的列表readable_list,有socket对象就读出来
    # 读取数据
    for soc in readable_list:   # type:socket.socket  # 这个可以让soc有socket对象的提示
        try:
            if soc == server: # 判断如果是等待连接的,就接收连接,并把客户端socket对象加到检测列表中
                client,addr = server.accept()
                rlist.append(client)
            else:  # 如果不是server,那就说明是客户端的socket对象可读了,读取数据
                data = soc.recv(1024)
                if not data:
                    soc.close()     # 检测到对面正常退出,那就把socket对象close并且移出检测列表
                    rlist.remove(soc)
                    continue
                if soc not in wlist:  # 判断如果socket对象不在检测可写列表中就添加,在就不管
                    wlist.append(soc)
                    msgs.append((soc,data)) # 将socket对象和数据存到容器中
        except ConnectionResetError:
            soc.close()
            rlist.remove(soc)   # 抓捕到对面强退错误后,关闭socket对象,并且从rlist中移除
            if soc in wlist:        # 并且需要判断一下这个socket对象是否在可写的检测列表中,在就移除
                wlist.remove(soc)

    # 循环可写的列表writeable_list,有socket对象就发送数据
    # 发送数据
    for soc in writeable_list: # type:socket.socket  # 这里面不可能有server,所以不需要判断
        for msg in msgs[:]:
            if soc == msg[0]: # 判断对象相同
                soc.send(msg[1].upper())
                msgs.remove(msg)  # 发送完就可以把信息从保存列表中移除
        wlist.remove(soc)
        # 如果这个socket对象需要发送的信息全部发送完之后,需要把他移出wlist检测
        # 因为select检测send就是如果缓存区没满,就会给你返回可写
        # 你如果不删除,会导致select会一直循环给你发可写信息
服务器端

服务器端需要注意的几个细节

1、select.select()函数中几个参数的意思

  rlist: 里面存储检测是否可读(recv、accept)的socket对象

  wlist: 里面存储检测是否可写(send)的socket对象

  xlist: 存储你需要关注的异常条件,忽略

  timeout:设置超时时间,如果超过设定时间还没有检测到可读或者可写,就返回空列表(一般不设置)

2、server也是一个socket对象,也需要检测accept,如果可读,就操作建立连接

3、如果检测到客户端socket对象正常退出或者异常退出,都需要把socket对象移出检测可读列表,

   在这里需要注意的是,当你把socket对象移出检测可读列表时,一定要判断一下,检测可写列表

   里有没有该socket对象,如果有就一起移除,避免下面发送数据出错,没有就不用移除

4、当检测出一个socket对象可写时,当把所有需要发送的数据发送完毕后,需要把该socket对象

   移出检测可写列表

   因为select检测可写,就是看缓存区,如果缓存区没满,他就返回给你可写,如果你不移除,

   会造成死循环(告诉你可写,你又没东西写)

原文地址:https://www.cnblogs.com/hesujian/p/10998612.html