我知道的 TCP

或者说,大家都知道的 TCP 知识

1起步
2未完待续......

起步

TCP 是一种面向连接的、可靠的、基于字节流的传输层协议。[1]

我们知道 IP 协议仅仅提供了一个不可靠的包交换语义。但是现实中我们往往需要一个可靠的通道来传输数据,以免数据丢失,比如说和数据库交互、或者浏览网页。为了在一个不可靠的协议上保证我们的可靠性,我们往往会选择使用 TCP(当然我们也可以使用 QUIC 或者其他协议,但是 TCP 目前还是相对更为主流的选择)—— 即一个构建在 IP 协议上的可靠传输层协议。

为了保证数据被可靠地发送和接收,确保其被正确传输,并且确保数据包的顺序正确,TCP 协议是一个有状态的协议。

它会将应用层递交的数据视为一段没有天然边界的连续字节流。连接建立通常通过三次握手完成,连接关闭通常通过四次挥手完成。为了保证可靠传输,TCP 使用序列号、确认应答、重传、校验和以及窗口等机制(见之后的 “内部实现” 了解更多)。

例子 —— 使用 netcat 建立 TCP 连接

在 Linux 中我们经常会使用到管道,用于在同一个机器上的两个进程之间进行通信。比如我们可以:

cat foobar.txt | gzip > foobar.txt.gz

这里,cat 命令读取 foobar.txt 文件,并将字节流写入到管道中,而 gzip 命令从管道中读取、压缩,并写入到标准输出(重定向到了文件 foobar.txt.gz)。

这里的管道语义和 TCP 在应用层暴露出来的抽象有些相似,即一种可靠的字节流通道,不过 TCP 是基于网络的。这样,某一个应用可以将一些字节流递交给 TCP 协议,TCP 协议会自动将其划分为报文段,并委托 IP 协议发送。而另外一个应用就可以基于一种字节流抽象来使用 TCP 接收这个字节流,并进一步处理。

Linux 的管道中,我们只能在同一个机器的两个进程之间进行 IPC 通信,不过,我们可以使用 netcat 来构建 TCP 连接,使得我们可以跨机器来构建一个 “管道”。

一般我们将等待接受 TCP 连接的称为服务端(server),而主动发起 TCP 连接的称为客户端(client)。这里 server 端会监听(listen)给定的端口并等待建立 TCP 连接。首先在服务端,我们需要先监听 TCP 端口,比如:

nc -l4 -p 12345 | gzip > foobar.txt.gz

这里,netcat 监听 IPv4 的 TCP 端口 12345,并将 TCP 字节流写入到管道中。需要注意的是,不同 netcat 实现的命令行参数略有差异。

如果服务端的 IPv4 地址为 192.168.1.100,那么我们可以在客户端使用 netcat 来建立连接:

cat foobar.txt | nc 192.168.1.100 12345

使用 netcat,我们构建了一个 TCP 连接,其中,在客户端机器上,我们读取 foobar.txt 文件,使用 TCP 协议将其传输到服务端,服务端进一步接收这些字节流,并使用 gzip 工具压缩,并写入到 foobar.txt.gz。这里可以看到 TCP 连接和 Linux 管道惊人的一致性。

例子 —— 使用 Python 与 TCP 交互

这里我们使用 Python 标准库 socketserver 来构建一个服务器:

from socketserver import BaseRequestHandler, TCPServer

class EchoHandler(BaseRequestHandler):
    def handle(self):
        print('Got connection from', self.client_address)
        while True:
            msg = self.request.recv(8192)
            if not msg:
                break
            self.request.sendall(msg)

if __name__ == '__main__':
    serv = TCPServer(('', 20000), EchoHandler)
    serv.serve_forever()

这里 server 端在监听 20000 端口,当建立了 TCP 连接之后我们就使用 EchoHandler 这个类来处理连接了。它会从这个 TCP 模拟出来的管道里:

  • 使用 request 字段的 recv 方法,从 TCP 连接中读取字节流,最多不超过 8192 字节。这个方法在有数据可读时会立即返回;如果暂时无数据可读,它会阻塞直到有数据到来,或者连接被关闭。
  • 使用 request 字段的 sendall 方法写入回去。

之后我们可以来一个 client 段,它和 server 端类似,不过一个是被动打开的,一个是主动打开的:

>>> from socket import socket, AF_INET, SOCK_STREAM
>>> s = socket(AF_INET, SOCK_STREAM)
>>> s.connect(('localhost', 20000))
>>> s.send(b'Hello')
5
>>> s.recv(8192)
b'Hello'
>>>

一个监听端口上可以同时存在多个 TCP 连接。内核通常使用源 IP、源端口、目的 IP、目的端口这个四元组来区分不同的连接,这使得我们可以在一个端口上为不同的客户端提供服务。这样,我们就提供了一个 echo 服务。

链接和引用

  1. Wikipedia / TCP: https://zh.wikipedia.org/wiki/%E4%BC%A0%E8%BE%93%E6%8E%A7%E5%88%B6%E5%8D%8F%E8%AE%AE
  2. 《TCP/IP 详解 -- 卷 1:协议》,[美] Kevin R. Fall, W. Richard Stevens 著,吴英等译,机械工业出版社.
  3. Python Cookbook / 建立 TCP 服务器: https://python3-cookbook.readthedocs.io/zh_CN/latest/c11/p02_creating_tcp_server.html