基于IO复用(非阻塞IO)实现的 netcat

使用非阻塞IO可以有效避免上述情况的发生。但非阻塞IO在编程上要比阻塞IO更难,并且在程序的维护上比较痛苦。一般使用非阻塞IO编程时建议使用一些封装好的网络库比较容易编写。

代码

  • recipes/python/netcat-nonblock.py
  • netcat-nonblock
    #!/usr/bin/pythonimport errno
    import fcntl
    import os
    import select
    import socket
    import sys
    # 设置非阻塞
    def setNonBlocking(fd):flags = fcntl.fcntl(fd, fcntl.F_GETFL)fcntl.fcntl(fd, fcntl.F_SETFL, flags | os.O_NONBLOCK)# 非阻塞的写数据
    def nonBlockingWrite(fd, data):try:nw = os.write(fd, data)return nwexcept OSError as e:if e.errno == errno.EWOULDBLOCK:return -1def relay(sock):socketEvents = select.POLLINpoll = select.poll()poll.register(sock, socketEvents)poll.register(sys.stdin, select.POLLIN)setNonBlocking(sock)# setNonBlocking(sys.stdin)# setNonBlocking(sys.stdout)done = FalsesocketOutputBuffer = ''while not done:events = poll.poll(10000)  # 10 secondsfor fileno, event in events:if event & select.POLLIN:if fileno == sock.fileno():        # 套接字可读(该文件描述符号 等于 网络链接套接字文件描述符)data = sock.recv(8192)if data:nw = sys.stdout.write(data)  # stdout does support non-blocking write, thoughelse:done = Trueelse:assert fileno == sys.stdin.fileno()  # 标准输入可读data = os.read(fileno, 8192)       # 将输入读到date中if data:assert len(socketOutputBuffer) == 0   # 读之前确认buf中没有数据,否则可能会造成数据乱序nw = nonBlockingWrite(sock.fileno(), data)   # 写数据if nw < len(data):  # 如果数据没有写完,需要保存剩余的数据到 scoketOutputBuffer中if nw < 0:nw = 0socketOutputBuffer = data[nw:] # 暂存没有写完的数据socketEvents |= select.POLLOUT  # 开始关注 pollout 事件(此时socketEvents 同时有sock的读写事件)poll.register(sock, socketEvents)   # 重新注册sock,关注sock的读(接收对端数据)和写(本端有未发送完的数据)poll.unregister(sys.stdin)  # 不再关注stdin事件else:  # 如果没有标准输入,则表示可以关闭连接了sock.shutdown(socket.SHUT_WR)poll.unregister(sys.stdin)if event & select.POLLOUT:if fileno == sock.fileno():  # 判断是否是网络套接字上的事件assert len(socketOutputBuffer) > 0   # 断言,只有当socketOutputBuffer有剩余的时候,才需要向sock写数据nw = nonBlockingWrite(sock.fileno(), socketOutputBuffer)# 向sock文件描述符写数据if nw < len(socketOutputBuffer):   # 如果仍然没写完,则保留余下的数据,继续重复上述操作assert nw > 0socketOutputBuffer = socketOutputBuffer[nw:]else:                             # 如果数据写完了,则套接字上的写事件不需要了socketOutputBuffer = ''socketEvents &= ~select.POLLOUT    # 取消套接字写事件标记,此时的socketEvents == select.POLLINpoll.register(sock, socketEvents)# 重新注册套接字事件(关注套接字上的读事件)poll.register(sys.stdin, select.POLLIN) # 关注,标准输入上的读事件def main(argv):if len(argv) < 3:binary = argv[0]print "Usage:\n  %s -l port\n  %s host port" % (argv[0], argv[0])print (sys.stdout.write)returnport = int(argv[2])if argv[1] == "-l":# serverserver_socket = socket.socket(socket.AF_INET, socket.SOCK_STREAM)server_socket.setsockopt(socket.SOL_SOCKET, socket.SO_REUSEADDR, 1)server_socket.bind(('', port))server_socket.listen(5)(client_socket, client_address) = server_socket.accept()server_socket.close() # 关闭监听套接字relay(client_socket)else:# clientsock = socket.create_connection((argv[1], port))relay(sock)if __name__ == "__main__":main(sys.argv)
    
  • 因为设置为非阻塞IO后,写操作会有无法将全部数据写完的情况发生(short write),因此在发生数据未写完时,我们再注册一个写事件,让程序将未写完的数据全部写完

程序结构

测试

  • 测试结果:可以看到这次,即使 nc 上有输入也不会阻塞程序

非阻塞IO 中的 short write


如何处理非阻塞IO中的 short write

一般来说在非阻塞编程中:

  • 对于非阻塞的读,如果读数据不全,我们需要将数据缓存,等凑够一条完整消息再触发消息处理逻辑。
  • 对于非阻塞的写,通常是网络库需要实现的功能,我们需要做的是告诉网络库我们需要发送多少数据,至于网络库内部如何处理事件我们在编程阶段不关心。

以发数据为例:

  • 如果数据发送不完整,剩下的数据需要放置到一个发送的缓冲区中。

    • 如果缓冲区非空,则我们不能对新数据write。
      因为这会造成数据乱序。只有等上一条消息全部发送成功后才可以对新消息进行发送。
      方案一:先尝试发送一次数据,如果数据发送不完整,将剩余数据存放在发送缓冲区(应用层的),然后注册POLLOUT事件用于处理剩余的数据发送。
  • 或者始终从缓冲区发送数据。
    方案二:将所有数据都存放在发送缓冲区,然后再去注册POLLOUT事件,只通过POLLOUT事件处理数据的发送。
  • 向套接字中写数据,关注POLLOUT事件。
    • 当POLLOUT准备好时,开始从发送缓冲区(应用层)中取数据向sock中写
    • 当发送换缓冲区(应用层)为空时,停止关注POLLOUT事件。
      注意,如果忘记取消关注POLLOUT事件,则认为sock一直可写(LT模式持续通知我们有事件),而实际上我们并没有数据向sock写,会进入一种busy loop状态,大量空耗cup过度占用资源。

对方接收数据缓慢


设想,本端在发送一个文件时,将文件加载到内存中然后通过网络向对端发送,而如果对方接受缓慢,那本端的发送方就要迁就对方从而缓慢发送,但是本端内存中缓存的数据就会持续占用在内存中。

如果待发送的数据很多的话,那么一味的将文件读取到内存中。等待向对端发送显然是不明智的。因为这将会占用大量的内存资源。

这种场景类似于水池灌溉模型,即一个水池一边放水一边注水。放水的下流出口用于灌溉农田,注水的上流入口取自水库抽取的水源。假设现在要达到灌溉最大化,既不浪费水源,又能以最大速率灌溉农田。那么由于 V 注水 !=V 放水,必然会产生下面这两种结果:

  • 如果注水量大于放水量,那么一段时间后池子将会溢出(浪费水源)。
  • 如果注水量小于放水量,那么一段时间后池子内将不会存留下水(灌溉效率没有达到最大化)。
    .
    最好的状态是池子内永远有一定量的水位,这样从池子内流出的水将以最大速率流出灌溉农田,同时也不会浪费水源。
    .
    那么解决方案就是认为设定一个最高/低水位(hight/low water mark),如果水池内的水位高于最高水位则停止注水,如果水位低于最低水位则开始注水。这样使得水位在 hwm ~ lwm 之间浮动,而水池出水率始终是最大值。

同理,我们可以参考高低水位的方式去设计。例如在向对端发送数据时,内存中缓冲的数据已经高出规定阈值时,我们可以考虑不去读对方的下一个请求直到本次发送完成,并且可以限制从本地读取待发送文件的速率。

但这终究不是一个完美的解决方案,对于接收端大量频繁的请求而言,我们不 read() 这些请求并不是一个好的解决方案,最好的方式是接收两方进行协议层面的商定,通过滑动窗口的思想告知接收端是否可以开启下一次的请求。这样方可避免由于接收方大量的数据请求而造成发送端发缓冲区数据的大量堆积(比如,接收端每次get image请求,发送端将对应image数据发送给接收端。对于接收端而言发送一次请求耗费的数据量很少,而发送发要回应的每个请求的数据量等很大,如果发送方一次接收到多个请求则这些应答数据将会占满发送发的缓冲区)。

LT 和 ET 模式

  • select() 与 poll() 都是 LT 模式。 如果有事件到达,还没有进行处理,则它会一直通知,直到事件被处理。
  • epoll() 即 edge-poll。 它同时支持 LT 模式与 ET 模式。
  • 能否结合两种模式的特点,分别在不同的场景使用不同的模式
    • 对于ET模式而言, 更适用于write事件和accept事件(accept如果文件描述符用完,会陷入死循环中,因此使用模式更好)
    • LT模式 更适用于read()事件,它不会造成接收的饥饿,ET模式可能会造成数据接收不完整的情况。
    • 可惜的是,目前内核中使用同一种数据结构表示读和写事件,读写事件放在一个就绪列表中,在读出后才判断是读事件还是写事件。因此,我们无法实现在不同的场景使用不同的模式。 值得注意的是,许多第三方网络库都使用的是LT模式,一般来说为了互相的兼容推荐使用LT模式。

Linux C/C++网络编程实战-陈硕-笔记20-使用非阻塞IO相关推荐

  1. Linux C/C++网络编程实战-陈硕-笔记15-如何正确使用 TCP

    netcat netcat 除了读取 socket 描述符,还要读取 stdin 和 stdout 并发模型 thread-per-connection IO 复用和非阻塞 IO 配合 如何安全的关闭 ...

  2. Linux C/C++网络编程实战-陈硕-笔记3-回顾基础的 Sockets API

    性能指标 带宽(Bandwidth):每秒收发的数据量,MB/s.(只关注数据量,不关注消息数) 吞吐量(Throughput):消息/s.查询/s(QPS).事物数/s(TPS) 延迟(Latenc ...

  3. Linux C/C++网络编程实战-陈硕-笔记10-网络时间同步

    网络时间同步(NTP)原理 Device A发送一个NTP报文给Device B,该报文带有它离开Device A时的时间戳,该时间戳为10:00:00am(T1). 当此NTP报文到达Device ...

  4. Linux C/C++网络编程实战-陈硕-笔记2-一个TCP的简单实验

    实验环境 命令 dd命令: 用于读取.转换并输出数据. 可从标准输入或文件中读取数据,根据指定的格式来转换数据,再输出到文件.设备或标准输出. 参数: if=文件名:输入文件名,默认为标准输入.即指定 ...

  5. Linux C/C++网络编程实战-陈硕-笔记11-Roundtrip代码分析

    代码 UDP, two threads recipes/tpc/roundtrip_udp.cc UDP with muduo, single thread muduo/examples/roundt ...

  6. Linux C/C++网络编程实战-陈硕-笔记17-多个版本的Netcat概览

    操作模式 对于 netcat 程序,存在两种模式,即 服务端 和 客户端, 它们的区别在于连接建立的方式. 一旦连接建立,客户端/服务器 的行为都是一样的,使用两个并行的循环处理: 从标准输入,写到 ...

  7. Linux C/C++网络编程实战-陈硕-笔记7-TCP自连接

    TCP 自连接 tcp连接两段使用了同一端口进行连接,而tcp并没有报错并且连接成功.即localhost:x --> localhost:x 只存在于 tcp 本地通信,而且客户端先于服务端启 ...

  8. 网络编程实践陈硕笔记零

    1.理查德森的unix网络编程有两样不足:一消息格式处理,特别是非阻塞IO上处理TCP分包问题:二是并发模型稍显陈旧,传统高并发采用事件驱动加非阻塞IO方式. 2.程序员面对的网络术语 Etherne ...

  9. 【网络编程实践--陈硕】学习笔记 | 汇总目录

    本文参考<网络编程实践>–陈硕(Muduo作者)视频课程所写.基于课程内容所做归纳和整理. <网络编程实战>配套页面:http://chenshuo.com/pnp Blog ...

最新文章

  1. linux下编译动态和静态链接库
  2. i12单双耳切换_多种形态,切换惬意,雷柏i100蓝牙TWS耳机,支持单耳/双耳使用...
  3. 一百种简单整人方法_一种非常简单的用户故事方法
  4. 循环机换变速箱油教程_汽车变速箱油啥时更换?重力换和循环机换有啥区别?注意啥?...
  5. html 滚动条,菜鸟,ionic 滚动条
  6. 剑指offer之顺时针打印矩阵
  7. js操作url的常用函数
  8. dell重装系统后找不到无线网卡驱动
  9. QWT官方例子--barchart
  10. c语言指针 汇编间接寻址,C语言指针和汇编语言间接寻址的关省略探讨从存储空间图的视角加以分析.pdf...
  11. 正点原子i.mx6ullMini开发板用SPI驱动RC522门禁卡模块
  12. (转)高盛报告:人工智能、机器学习和数据将推动未来生产力的发展
  13. 计算机组成CPU最佳配置,计算机组成原理--CPU
  14. 【Python学习笔记】猜颜色小游戏
  15. RSR 服务器 进行RTMP推流报错:Failed to update header with correct duration Failed to update header with
  16. 一个可以不被广告拦截器拦截的弹出窗口
  17. 开课吧 Linux与基础系统编程
  18. DL4J模型训练Word2Vec
  19. 监控需求以及开源方案的对比
  20. DisplayPort--Link training之Clock Recovery (CR)

热门文章

  1. 达芬奇无声音解决方案
  2. app使用经验分享——墨墨背单词
  3. 一个编程菜鸟对未来编程学习的看法
  4. 如何将一节课转成文字文稿
  5. pdf转换成jpg教程
  6. Github教程】史上最全github使用方法:github入门到精通
  7. zip的使用--组合坐标
  8. html聚焦标签,HTML
  9. 关于Linux运维大环境
  10. 项目需求规格说明书(国标) 一般格式