• day 39 I/O模型之阻塞I/O模型、非阻塞I/O模型、多路复用


    今日内容:阻塞I/O模型、非阻塞I/O模型、多路复用

    阻塞、非阻塞:程序的运行状态。

      阻塞:指遇到I/O操作;非阻塞:指运行态与就绪态

    同步、异步:任务的提交方式。

      同步:指提交任务后在原在等待,等任务运行完毕后再执行其他代码;

      异步: 提交任务不用等,直接运行其他代码,通常与回调函数一起使用

    I/O模型:模型就是一种解决问题的套路方法 ,I/O模型解决的是在单线程下的I/O问题       

     为了解决的是服务端的网络I/O,I/O行为:send / recv / connect / accept

    收数据会经过两个I/O阶段:【waite_data】 等待收客户端的数据、【copy_data】从操作系统拿到数据后,应用程序再从缓存区读到应用程序内存中

    发数据会经过的I/O阶段:应用程序将数据copy给系统缓存: copy_data --  本地I/O速度非常快,但数据量比较大操作系统缓存区被占满时,就会阻塞。

    waite_data:相比copy_data耗时最长

    copy_data:时间很短

    一、阻塞 I/O模型:程序在遇到I/O阻塞时【waite_data、copy_data时】程序都需要在原地等待,什么事都干不了效率底下。默认情况下就是I/O阻塞,

      处理方式一:accept ,  recv、send时程序都需要在原地等待【waite_data、copy_data 阶段】,建立链接- 收发完成,循环处理同一时间只能服务一个客户端,没有并发效果。

      处理方式二:利用多进程accept交给一个进程,链接成功后将链接成功的客户端socket对象交给另一个进程处理【收发数据】实现了并发效果 --  因为进程开销大,通信不便利 --线程

            --  实现了并发

      处理方式三:线程不可能无限开,--  线程池,限制线程数量,保证机器正常运行,当并发量巨大处于I/O阻塞态的线程数大于池中限制数时,多出来的链接就无法链接上 -- 

             gevent模块:测检I/O并在I/O阻塞时切换到另一个任务上,单线程并发【协程】极大的提高并发:

    二、非阻塞I/O模型Nonblocking model  :

        server.setblocking(Fasle) 设置为非阻塞【默认是阻塞的】, 不停的建链接,当收到错误信息或者说没有数据时就处理其他任务。有链接就将此链接保存到列表,

        再遍历列表尝试发送接收数据,可以发就发,不能收发就捕获异常【BlockingIOError:continue】【ConnectionResetError:conn.close(); r_list.remove(conn)】,

        不停的建链接-处理数据循环处理实现并发处理。

        问题:每次读取数据时  不一定有数据,为了能够及时数据,需要不停的忙轮询,忙轮询这种方式会在没有数据需要处理时也需要不停的循环,造成了CPU无功占用

    import socket

    import time

    s = socket.socket()
    s.bind(('127.0.0.1', 8080))
    s.listen(5)

    s.setblocking(False) # 默认为阻塞,设置为False表示非阻塞
    clinents = [] # 用来存储客户端的列表
    msgs = [] # 用来存储需要发送的数据和客户端的对象

    while True: # 链接客户端的循环
    try:
    clinent, addr = s.accept() # 接受三次握手信息
    print('来了一个客户端了... %s' % addr[1])
    #有人链接成功了
    clinents.append(clinent)
    except BlockingIOError as e:
    print('还没有人连过来。。。')
    time.sleep(0.01) # 需要制造I/O来缓解CPU的高速运转
    #收数据的操作
    for c in clinents[:]:
    try: #可能这个客户端还没有数据过来
    #开始通讯任务
    date = c.recv(1024)
    if not date:
                c.close()
    continue() #当为空时不用执行msgs.append((c, date)
                        #raise ConnectionResetError()
    #c.send(date.upper()) 如果碰巧缓存区满了,这个数据就丢失了
    #由于此处捕获了异常 所以应该单独来处理发送数据
    msgs.append((c, date))
    except BlockingIOError:
    print('这个客户端不需要处理')
    except ConnectionResetError:
    #断开后删除这个客户端
    c.close()
    clinents.remove(c)

    for i in msgs[:]: #发送数据的操作
    try:
    c, msg = i
    c.send(msg.upper())
    msgs.remove(i) #如果发送成功!删除这个数据
    except BlockingIOError:
    pass

    三、多路复用:select.select()方法可以不停检查是否有数据,会返回需要处理的链接对象列表

        对比线程池:避免了开启线程的资源消耗

        缺点:同时检测socket数不能超过1024

        为什么是1024个原因如下:

            1.需要给每一个socket的等待队列中添加进程信息

            2.唤醒进程后,进行必须遍历所有socket 才能知道哪些socket有数据了

            3.还要遍历所有soket将进程从等待队列中删除

              上述操作,会大量消耗系统资源,所以必须限制同时处理的socket数据

    import socket
    import time
    import select

    s = socket.socket()
    s.bind(('127.0.0.1', 8080))
    s.listen(5)

    """
    参数1: rlist里面存储需要被检测是否可读(是否可以执行recv)的socket对象
    参数2: wlist里面存储需要被检测是否可写(是否可以执行send)的socket对象
    参数3: xlist 存储你需要关注异常条件 -- 忽略即可
    参数4: timeout 检测超时时间 一段时间后还是没有可以被处理的socket 那就返回空列表
    返回值:三个列表
    1.已经有数据到达的socket对象
    2.可以发帝数据的socket对象 怎么可以发 缓冲区没有满
    3.忽略
    """
    read_list = [s] #将需要检测(是否可读== recv)的socket对象放到该列表中
    #accept也是一个读数据操作,默认也会阻塞 也需要让select来检测
    #注意select最多能检测1024个socket超出直接报错

    write_list = [] #将需要检测(是否可写== send)的socket对象放到该列表中
    #只要缓冲区不满都可以写

    msg_list = [] #存储需要发送的数据和socket 等待select检测后再进行发送即write_list中的client

    print('start')
    while True:
    rlist, wlist, xlist = select.select(read_list, write_list, [])
    print('可读的有%s个,可写的有%s个' % (len(rlist),len(wlist)))
    """
    readable_list 中存储的是已经可以读取数据的socket对象 可能是服务器 可能是客户端
    """
    #处理可读的socket
    for sec in rlist:
    if sec == s:
    #服务器的处理
    clien, addr = sec.accept()
    #将新链接的socket对象加入到待检测列表中
    read_list.append(clien)
    else:
    try:
    #收数据,客户端的处理
    data = sec.recv(1024)
    if not data:
    # sec.close()
    # rlist.remove(sec)
    # continue
    raise ConnectionResetError()
    #不能直接发,因为此时缓冲区可能已经满了,导致send阻塞住,所以要发送数据前将这个socket交给select来检查
    write_list.append(sec)
    #将要发送的数据先存起来
    msg_list.append((sec,data))
    except ConnectionResetError :
    print('非正常退出')
    sec.close()
    #对方下线后,应该从待检测列表中删除socket
    read_list.remove(sec)
    if sec in write_list:write_list.remove(sec)
    #处理可写列表
    for w in wlist:
    #由于一个客户端可能有多个数据要发送,所以遍历所有客户端
    for i in msg_list[:]:
    if i[0] == w:
    w.send(i[1])
    #发送成功,将这个数据从列表中删除
    msg_list.remove(i)
    #数据已经都发给客户端 这个socket不需要再检测是否可写,必须要删除
    write_list.remove(w) #否则 只要缓冲区不满 一直处于可写导致死循环
  • 相关阅读:
    Cartographer源码阅读(1):程序入口
    ROS开发与常用命令
    实时Cartographer测试(1)
    Cartographer安装
    ROS安装(2)
    Linux学习和ROS安装(1)
    无法启动程序
    c# 获取端口的连接数,网站的连接数
    SignarL服务器端发送消息给客户端的几种情况
    c#操作IIS之IISHelper
  • 原文地址:https://www.cnblogs.com/qingqinxu/p/10999914.html
Copyright © 2020-2023  润新知