21-IO复用方式实现netcat

IO复用

  • IO复用也称:事件驱动、基于事件、reactor。其实它复用的不是IO,而是复用线程,简单理解为让一个线程处理多个IO。注意:这种方式是同步的,不是异步的。
  • 只用一个线程,自带的netcat也是以这种方式实现。
  • 通常IO复用应该搭配非阻塞IO,但为了说明为什么,下面的代码使用阻塞IO(一个方向可能会阻塞另一个方向)。
  • 磁盘文件的文件描述符:被动地,程序可以决定什么时候去读写,不会遇到阻塞情况(没有数据才会阻塞)。
  • socket的文件描述符:主动地,程序不可以决定什么时候去读写。所以引用IO复用,先查一下文件描述符是否可读写,然后再进行读写。

代码

recipes/netcat.py at master · chenshuo/recipes (github.com)

#!/usr/bin/python

import os
import select
import socket
import sys

def relay(sock):
    poll = select.poll()
    poll.register(sock, select.POLLIN) # 注册读事件
    poll.register(sys.stdin, select.POLLIN)

    done = False
    while not done:
        events = poll.poll(10000)  # 10 seconds
        for fileno, event in events:
            if event & select.POLLIN:
                if fileno == sock.fileno(): # socket有数据
                    data = sock.recv(8192)
                    if data:
                        sys.stdout.write(data)
                    else:
                        done = True
                else: # stdin有数据
                    assert fileno == sys.stdin.fileno()
                    data = os.read(fileno, 8192)
                    if data:
                        sock.sendall(data)
                    else:
                        sock.shutdown(socket.SHUT_WR)
                        poll.unregister(sys.stdin)


def main(argv):
    if len(argv) < 3:
        binary = argv[0]
        print "Usage:\n  %s -l port\n  %s host port" % (argv[0], argv[0])
        return
    port = int(argv[2])
    if argv[1] == "-l":
        # server
        server_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:
        # client
        sock = socket.create_connection((argv[1], port))
        relay(sock)


if __name__ == "__main__":
    main(sys.argv)

测试

  • server:chargen,白洞(只发数据,不收数据)
  • clinet:nc: nc < /dev/zero

chargen 程序只发送数据而不读取,如果使用 nc 时有标准输入,即 nc 会向 chargen 发送数据,将最终导致 chargen 的接收缓冲区被填满,而 nc 无法再发送数据。

因此,nc 的实现方式就显得尤为重要了。如果 nc 这边是网络读写没有分离开,那么由于对端缓冲区满将会导致本端写动作阻塞,进而阻塞整个程序。

用自带的netcat

  • 用c++的chargen作服务端,用自带netcat作客户端。第一次只带输出,不带输入。发现吞吐量为1.3GB/s。第二次带上输入(< /dev/zero)发现收到一些数据后就停了。

image-20230218113054114

  • 用strace命令查看原因(重新运行一遍第二次的情况)

image-20230218113429611

image-20230218113447748

发现阻塞在write上。

netstat查看原因

由于chargen只发不收,导致缓冲区很快就满了,从而使得netcat阻塞在写socket上(write)

image-20230218113533964

用python的netcatimage-20230218113830748

原因和上面的一样。(会读也会写,但是用的是阻塞IO)

用thread per connection

image-20230218114037134

(读写各一个线程,读写操作阻塞,这里最后只会阻塞在写操作,读操作不会阻塞)。这里因为读写是各自的线程互不影响,所以chargen的输出不会停。

posted @ 2023-04-29 15:49  DavidJIAN  阅读(20)  评论(0)    收藏  举报