TCP 编程(三)

215 阅读3分钟

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

先前我们实现了经典的请求回应模型。并且提了与它类似的最初的 HTTP 协议。他们最大问题在于对全双工连接的浪费。

回忆我们的实现代码,是一个相当典型的同步过程,以客户程序为例,它发送数据之后,就等待着数据的返回。 这样的代码会造成阻塞,而阻塞的结果则会使得后续的每一个待发送的数据都要等前面的数据返回之后才能处理。

这是极大的浪费,因为 TCP 是全双工的,它本身支持同时读和写,所以一个比较自然的想法就是两边都同时做这件事,也就是并发程序,实现的方式实际上是比较多的。进程、线程、协程都可以。

这里我使用linux自带的fork系统调用(这个函数在C语言中行为基本一致),通过fork函数可以把一个进程分叉成两个,然后他们都会拥有原来的变量等数据。 对于客户程序,程序如下:

pid = os.fork()
while True:
    if pid == 0:
        client.send("data from client".encode())
    else:
        recv = client.recv(buffer_size)
        print('从服务端接受到的数据: ', recv.decode())

而服务端对应于这个连接的程序则像下面这样:

pid = os.fork()
while True:
    if pid == 0:
        data = conn.recv(buffer_size)
        print("从接受到的数据: ", data)
    else:
        conn.send("data from server".encode())

现在一切看起来都挺美好,程序也是可以运行的。然而,这里面其实是有大坑的。还记得最开始描述TCP的特点吗?TCP是面向连接的,外部表现也是这样,数据会像水流一样传输,前面的请求回复模型,因为双方都是两个数据发送之间被间隔开来了,在简单的测试下没有暴露出问题。

上述的程序我们运行时会看到奇怪的现象,它的提示字符串后面会有一堆数据。而不是对方发来的单独一条数据。这个现象被称为 TCP 粘包。

而且这个问题实际上是不可以通过刷新缓冲区解决,当我们站在某一方的视角时,可能觉得我每一条消息刷新一次缓冲区然后可以看作是分成不同的消息。但是,当我们以另一方的视角来看时,即使数据确实是按消息达到的,问题是如果没有读取出来,它们同样会黏在一起。而且即便读取速度上能同步,读取的长度也是不确定的。

可以发现,问题是无法分辨消息。这需要双方的一些共识来实现,方法反而有很多。比如说我们使用定界符(这会涉及转义或者编码)。又或者我们采取定长的方式(这意味着长消息要拆分重组,短消息则会浪费)。还可以通过在开始一段数据指明数据长度,然后按照这个方式读取数据。

HTTP 协议对数据的传输,基本可以算是这几种的综合。

再回到我们刚才写的程序,其实它的逻辑是没问题的。要让它正确解析双向的数据,把数据按照某种方式解析,然后和数据的读取一同封装即可。

我们成功的利用了双工连接的特性,也想了办法解决粘包的问题。但是实际上,我们没有引入语义。而引入语义也会再一次带来新的问题。来日方长,我们下次再解决。