9.10. TCP —— 可靠的字节流

TCP,即传输控制协议(Transmission Control Protocol),是建立在 IP 之上的另一种传输层服务。如果说 UDP 最贴切的描述是“发出一个数据包然后听天由命”,那么 TCP 则是“在两个端点之间打开一条连接,把它当作一根双向的字节管道,对端一定会按顺序、且只会恰好一次地收到这些字节”。互联网上的大部分流量都使用它,摄像头在网络上所做的大部分工作也都使用它。

9.10.1. TCP 在 IP 之上增加了什么

TCP 所做的远比 UDP 多。它增加了:

  • 连接。 在任何数据流动之前,两个端点会交换一次简短的握手,以确认彼此正在通信。连接具有状态——“打开”、“半关闭”、“关闭”——两端都会跟踪这个状态。

  • 可靠传输。 发送方放入的每一个字节都会被接收方确认。任何在超时时间内未被确认的内容都会被重新发送。应用程序永远不会看到丢失的字节;它看到的只是协议重新发送时产生的延迟。

  • 有序传输。 字节按照发送的顺序到达。即使数据包在接收方处乱序到达,TCP 也会在应用程序读取之前将它们重新排序。

  • 流量控制。 如果接收方速度较慢,发送方会被告知放慢速度;连接会适应较弱一端的速率。

  • 拥塞控制。 如果中间的网络开始丢包,发送方会退避,直到情况恢复。这可以防止任何单条连接拖垮一条已饱和的链路。

所有这一切都是自动完成的。应用程序使用的 Python API 只是 send(bytes)recv(n);TCP 在底层处理其余的一切。

9.10.2. 握手

在允许任何数据通过之前,TCP 连接通过三次交换来打开:

一张有两列的图,分别标记为“client”和“server”。一个从 client 指向 server 的箭头标记为“SYN”,然后一个从 server 指向 client 的箭头标记为“SYN-ACK”,再然后一个从 client 指向 server 的箭头标记为“ACK”。在这之下有一个粗箭头标记为“data flowing both ways”(数据双向流动)。

三次握手。一旦双方都确认完毕,连接即打开,数据便可流动。

客户端发送一个 SYN(同步,synchronise)数据包请求打开连接。服务器以 SYN-ACK(同步 + 确认)回复,表示接受。客户端发送最后一个 ACK 以确认。在这一来回之后,双方一致认为连接已打开,并已同步各自用于跟踪哪些字节已被发送和接收的计数器。

握手会在第一个有用的字节通过之前耗费一个往返时延的延迟。对于本地网络,这是一毫秒;对于跨大陆的连接,这大约是 100 ms。这是 TCP 的主要代价,也是简短的一次性消息有时改用 UDP 反而更好的原因。

9.10.2.1. 关闭握手

TCP 连接同样通过一次交换来关闭(每一方发送一个 FIN)。任何一端都可以独立地关闭自己那一半连接;一种 半关闭 状态——一方已结束发送而另一方仍在通信——是合法的,尽管并不常见。应用程序通常只需调用 close(),让协议处理关闭流程。

9.10.3. TCP 保证什么

有几件事人们有时以为 TCP 会做,但它其实不会:

  • 消息边界。 应用程序发送的是 字节流,而不是消息流。两次 send(b"hello") 调用可能作为一次 recv() 收到 b"hellohello",也可能作为两次大小不一的 recv()收到。如果应用程序需要消息分帧,它必须自己添加分帧(一个换行符、一个长度前缀,或其他什么)。例如,通过 TCP 发送 JSON 文档时,需要用换行符或某种其他标记来分隔每个文档。

  • 加密。 TCP 一路明文传送应用程序交给它的字节。如果内容需要保密,应用程序必须用 TLS 包裹这条连接(参见 加密套接字与 TLS)。

  • 身份验证。 TCP 确保字节完好无损地到达。但它对 发送了这些字节只字不提。身份验证同样是更高层关心的问题。

  • 静默连接上的存活检测。 如果双方都长时间不发送数据,连接在技术上仍然是打开的,但无法检测到对端已崩溃或消失。在需要时,可以在套接字上启用 Keepalive(保活)探测来解决这个问题。

9.10.4. 何时使用 TCP

对于几乎任何符合“客户端打开一条到服务器的连接,双方交换一些字节,完成后连接关闭”这种模式的会话,TCP 都是正确的答案。HTTP 和 HTTPS 请求、SSH 会话、数据库查询、文件传输、图像上传——全都基于 TCP。

只有当会话不符合那种模式时才考虑 UDP:可接受丢失的独立自包含消息、组播流量、时间同步、名称查找,或者握手代价过高的极端延迟敏感场景。

有了端口、UDP 和 TCP,传输层的内容就讲完了。同时暴露这两者的 Python API 在下一页:套接字对象