TCP 编程(六)

134 阅读4分钟

「这是我参与11月更文挑战的第16天,活动详情查看:2021最后一次更文挑战」。

先前我们已经介绍了select函数的流程,但是被没有真正意义上的使用,为此我们将实现一个简单的 echo 服务器作为实例。从某种意义上,这可以看作是和 http 类似的服务器。但是,我们这里的重点是使用IO多路复用,而不是实现高层协议。

首先有一点很重要,当我们写一个 socket 服务时,用于监听的 socket 它也是 ⌈ 可读 ⌋ 的,与建立连接的 socket 不同,它的读是读出一个新的连接(因为实际上和对端连接的 socket 与用于监听的并不是同一个字)。当有新的连接建立起来之后,它会变得可读(触发 select )。这在一定程度上,会让程序有比较统一的写法。

我们姑且先不考虑错误的处理,回到 select 的几个主要的形参,我们只考虑其中的两个,可读和可写。先前我们简单介绍 select 的时候,有意的忽略了可写性。但是事实上读和写都是会造成 IO 阻塞的。这里我们会都考虑。

由上面的分析,程序的主要部分大致已经有了框架,重点就在于让阻塞都被 select 统一处理,我们关注的是那些不会阻塞的操作,也就是条件已经具备之后的的处理。 所以大致的程序可以如下:

def run(self):
    while True:
        readable, writable, _ = select.select(self.readable, self.writeable, [])  # blocking 最初self.readable 只保留了用于监听的 socket
        self.handle_readable(readable)
        self.handle_writable(writable)

异常情况我们先做了忽略,然后考虑对满足条件的处理,由于这里使用的基于类的实现,参数只传递了一个,实际上这些函数是要对传递给 select 的参数做调整的。(在本例中,作为 echo 回显程序,读取之后,数据之后自然就不用读取了,也就是需要移除,对于可写对象同理。)

然后考虑处理可读 socket 列表,正如先前所说,我们先不考虑出错以及其他例外情况,这样这里就有两种 socket ,用来监听的 socket 、等待读取待处理数据的 socket 。对于监听 socket ,需要从中获取一个新连接,然后把新连接添加到 select 的参数可读取列表。读取数据则是把数据读出,然后移除出 readable 列表。

这里有个值得注意的点,当前采取的策略是读归读,写归写,然后它们之间有个隐藏的关系是写依赖读的数据,这意味着需要有空间暂存数据,并在后续让写部分能够获取。具体实现可以自行考量。

一个可能的做法是使用哈希表,把数据作为值,而键则是描述符。

所以读程序大致像下面这样:

def handle_readable(self, readable: List['socket']):
    for fd in readable:
        if fd is self.server:
            conn, _ = self.server.accept()
            self.readable.append(conn)
        else:
            data = fd.recv(512)  # 假定读出了所有数据
            self.set_item(fd, data) # 在这里看作是保存键值对
            self.readable.remove(fd)

然后是写数据,也就是把数据发送到客户端。正如前面所说,写的数据需要从某个位置拿到。再是发送数据,完成处理流程。所以写部分大致如下:

def handle_writable(self, writable: List['socket']):
    for fd in writable:
        data = self.get_item(fd)# 获取待处理数据
        self.remove_item(fd)
        fd.sendall(data)
        self.writeable.remove(fd)

到这里我们基本上成功的以 IO 多路复用的方式实现了一个 echo 程序,虽然它还没有容错能力,但是主要的部分我们已经解决了。其他的问题主要是 TCP 的状态管理机制,它提出了一些可读可写的状态,可能和我们预期不太一样。还有就是对于连接异常的处理。不过万变不离其宗,这些情况逻辑处理上是一样的,只是要额外增加一点分支。

与并行处理的方式不太一样的是,select 部分可以说是充当了一个中介。这使得读写过程分离了,于是读写之间也变得需要一个中介为他们传递数据。这稍微绕了些弯,也是我们需要注意的地方。但俗话说有得必有失,有些小波折也可以理解。