3.5 TCP
本节将详细介绍 TCP 连接的性质与原理。TCP 扩展了网络层的 IP 协议。在提供最基础的进程到进程的传输服务与报文段的差错检测服务之外,TCP 实现了两个应用进程之间 面向连接的、点到点的、可靠的、字节流式的、管道化的、流水线式的 传输服务。
概述
前面已经无数次提到过,TCP 被称作是 面向连接的(connection-oriented)。即两个进程在能够发送数据之前必须先相互“握手”,准备某些预备报文段,以建立确保数据传输的参数。此外,TCP 连接是一条 逻辑连接,其共同状态仅仅保留在两个通信端系统的 TCP 程序中。主机之间的网络元素(如路由器与交换机)无法也不会知道这条连接的存在,它们只负责传输 IP 数据报。连接建立与拆除的详细过程见下面 连接管理。
除此之外,TCP 连接总是 点到点的,即每条 TCP 连接只能有一个发送方与一个接收方,无法做到一个发送方向多个发送方发送数据,或是从多个接收方接收数据。
TCP 连接提供的是 全双工服务(Full-duplex Service),即数据可以在两个进程建立的 TCP 连接中双向流动。
应用层的应用通过套接字向下层的 TCP 实体传送数据流,穿过套接字的数据被存放到对应连接的 发送缓存 中。TCP 会不时从发送缓存中取出一块数据配上 TCP 头部后封装为 TCP 报文段并交付给网络层。TCP 单次可以取出数据的字节数受限于 最大报文段长度(Maximum Segment Size,MSS),MSS 一般根据物理网络发送的最大链路层帧长度(即 最大传输单元(Maximum Transmission Unit,MTU))设置,一般要保证 TCP 报文段加上首部长度能够放在一个 MTU 中。以太网的 MTU 为 1500 字节,去除 20 字节 IP 头部与 20 字节 TCP 头部后,MSS 的典型值一般为 1460 字节。
TCP 建立的字节流式通信指的是,TCP 将数据看作一个 无结构但有序 的字节流进行发送,其不会对发送的数据分别属于哪个应用层报文做区分,这需要上面的应用进程自行区分。
TCP 同样实现了 流量控制 与 拥塞控制,会根据接收方进程接收数据的能力与网络上路径的拥塞程度控制向网络中发送报文段的速度。具体分别会在 流量控制 与 3.6 拥塞控制原理 及 3.7 TCP 的拥塞控制 中讲解。
报文段结构
TCP 报文段由 20 字节的首部与长度不超过 MSS 的数据字段构成。TCP 首段的结构如下图所示:

TCP 相比 UDP 新增的字段的部分解释如下:
- 各 4 字节的 序号字段 与 确认号字段,用于实现可靠数据传输服务,详细解释见后。
- 2 字节的 接收窗口字段,用于实现流量控制 ,指示了接收方的接受缓存的空闲部分(接收窗口)大小。
- 4 bit 的 首部长度字段,指示以 4 字节为单位下,首部的长度。
- 可选、变长的 选项字段,用于发送方与接收方协商 MSS,在高速网络环境下作为窗口调节因子等用途。
- 6 bit 的 标志字段,指示某些控制信息,例如用于分组确认的 ACK 标志位,用于TCP 连接管理的 RST、SYN、FIN 标志位,用于基于网络辅助明确拥塞通告的拥塞控制 的 ECE 与 CWR 标志位,与现已经弃用的 PSH 与 URG 标志位。
前面提过,TCP 将数据看作一个无结构但有序的字节流。序号字段与确认号字段分别代表了该报文数据部分的 首字节在字节流中的编号 与期望从对方收到的 下一个字节的编号。确认号隐含了 累积确认 的思想,即 TCP 只会确认到第一个未接收到的字节为止的字节。当连接建立时,发送方与接收方 协商并随机选择字节流起始序号,以防止滞留在网络中的来自相同两台主机的一条旧连接中发送的报文被错误接收。
TCP 规范并没有规定当接收方接收到失序报文时会怎么做,不过实践中常采用的做法是接收方保留失序字节一段时间,向发送方发送报文要求中间缺少的字节并等待其到达。
需要注意的是,服务器对来自客户端数据的确认是捎带封装在服务器向客户端发送的报文段中的。
由于 Telnet 采用明文传输数据,极易遭受窃听攻击,现已被 SSH 等自带信息加密的协议取代。
Telnet 是一种用于在远程主机上登陆的应用层协议。在该协议中,用户键入的字符被发送给远程主机;远程主机接收到这些字符后,会回送每个字符的副本给用户并显示在用户主机的屏幕上。这被称作“回显”,用于确保用户发送的字符已经被远程主机收到并处理。
我们详细考察一下当用户在键盘上敲下一个字符时发生了什么:
假设客户端与服务器选定的字节流起始编号分别为 42 与 79。当用户输入字符 C 后,客户端将该字符封装在 TCP 报文中发送,其序号与确认号分别为 42 与 79,告知服务器本报文封装数据的起始字节编号为 42,已经服务器发送的收到 79 及之前的字节。
服务器收到该报文段并处理后,将字符 C 封装到 TCP 报文段中发送给客户端用于回显,报文中的序号与确认号分别为 79 与 43。含义同上
客户端接收到服务器发来的回显报文,向客户端发送没有有效数据,只是用于确认的报文,其序号与确认号分别为 43 与 80。

往返时间估计与超时
TCP 采取超时重传机制处理报文段的丢失问题,但如何选择超时时间是一个重要的问题。时间过短会引发大量不必要的重发;时间过长会导致接收方对真正发生丢失的分组做出反应时会比较迟钝。显然超时间隔应当大于连接的 RTT,但在当今互联网的环境下 RTT 并非一成不变,而会不断波动。因此有必要根据当前链路的环境自适应的计算出超时时间。其具体机制如下:
TCP 实体会选定一些用于测量往返延迟的报文段,测量该报文段的 RTT,记为
这样做是为了直接避免 重传二义性 带来的对于 SampleRTT 测量的失真。即,当发出某个报文段后发生了超时,重传该报文段,随后对该报文段的确认到达,但问题在于,如何确定该确认对应着先发送的报文段还是重传的报文段?显然发生重传的报文段的实际 RTT 无法被确定,因此不能纳入 EstimateRTT 的计算。这被称作 Karn 算法。实验数据表明,不使用该算法会使最终 SampleRTT 值降至正常值的50%,极大增加连续丢包风险。
SampleRTT 的会随当时的网络状况出现较大幅度的波动,我们对其做 指数加权移动平均(Exponential Weighted Moving Average,EWMA)。具体而言,设
将该递推式展开,容易得到在已经测量了
可以发现,旧的 SampleRTT 在整个 EstimatedRTT 中的权重会呈指数级快速衰减,而最近的 SampleRTT 所占权重更高。RFC 6298 给出的
不同的网络给 SampleRTT 造成的波动幅度不同,若网络连接稳定 SampleRTT 波动很小,则最终的超时时间也不应有大幅的变化。我们使用当次 SampleRTT 与 EstimatedRTT 的绝对偏差来衡量 RTT 的波动,DevRTT 同样使用 EWMA 来计算,即:
RFC 6298 给出的
综合 EstimatedRTT 与 SampleRTT,超时定时器的设定时间为:
超时定时器的初始默认时间一般为 1s。
一旦某个报文发送超时,超时定时器的设定时间将加倍,以免后续报文段过早出现超时。然而一旦收到了对某个报文段的确认或接收到了来自上层应用的新数据,加倍机制就会取消,超时计时器设定时间重新按照上面的方式计算得出。
可靠数据传输机制
前面也无数次提到过,TCP 在不可靠的网络层 IP 服务上实现了可靠的数据传递。IP 的服务是 尽力而为 的,其不保证数据的成功交付、按序交付或是完整交付。而 TCP 在此之上保证了数据以无损坏、无间隙、无冗余、按序的字节流方式传送。
TCP 的可靠数据传输机制是 3.4 可靠数据传输的原理 里介绍的 GBN 与 SR 的混合体。具体如下:
- TCP 使用 累积确认,具体见上面报文段结构中对于序号与确认号的介绍。
- TCP 只维护一个超时定时器,其与被发送的最老的未被确认的报文段相关联。一旦触发超时,只重发该最老的段。如果收到了对最老的段的确认(由于 TCP 是累计确认,该确认可能使用的是该段后面的段的序号),重置定时器与当前最老的未被确认的报文段关联(如果所有的段都已被确认就不会重置计时器)。
- TCP 没有规定收到失序段时应怎么做,许多的 TCP 实现会缓存这些失序报文。另有一种已经被广泛使用的机制被叫做 选择确认(Selective Acknowledgment,SACK),允许 TCP 对有选择地对失序报文段进行确认。
由于每当超时定时器触发超时时都会导致超时定时加倍,这可能会导致当某一个段连续超时时,接下来发送的数据所设置的超时周期过长,增加了对重发丢失报文的响应延迟。因此 TCP 通过利用 冗余 ACK,即发送方已经收到的确认来触发 快速重传 机制。具体而言,如果接收方收到了对某个数据的 3 个冗余 ACK(注意需要加上第一个正常确认的 ACK,一共是 4 个 ACK),则立刻重发冗余 ACK 指示的丢失报文段。
此外,接收方并不是在接收到一个报文段后立刻发送对该报文段的确认。可以推断出,在发送方一次发送大量报文段的情况下,这种做法会增加冗余 ACK 的数量。RFC 5681 对发送方生成 ACK 的策略建议如下:

流量控制
前面的讨论都集中于应对网络中有可能发生的各种问题。然而,端系统本身也有一些需要注意的问题。有时发送方会快速发送大量数据,尽管网络畅通,接收方可以快速收到这些报文段,但接收方上层的应用程序可能无法在短时间内读取并处理这些数据,这会导致接收方的接收缓存溢出造成数据丢失。针对这种情况,TCP 引入了 流量控制服务(Flow-control Service) 用于抑制发送方发送数据的速率,使得发送方的发送速率与接收方应用的读取速率相匹配。
流量控制与拥塞控制虽然结果都是抑制发送方发送数据的速率,但针对的情况完全不同,绝不能混为一谈:
- 流量控制用于处理 接收方应用程序 读取速度慢导致接收缓存溢出的问题
- 拥塞控制用于处理 IP 网络的拥塞 导致数据无法正常传输的问题
发送方维护如下两个变量:
LastByteSent:已发送的最后一个字节的编号LastByteAcked:已确认的最后一个字节的编号
接收方维护如下两个变量:
LastByteRead:应用进程从接收缓存中读出的最后一个字节的编号LastByteRcvd:TCP 实体收到的最后一个字节的编号
显然,LastByteSent 减去 LastByteAcked 即为仍未被确认的数据量,记接收方接收缓存的大小为 RcvBuffer,则接收缓存的空闲空间为 RcvBuffer - (LastByteRcvd - LastByteRead),记为 接收窗口 rwnd,被接收方捎带发送给发送方。发送方需要保证未被确认的数据量总不大于接收缓存的空余空间,这保证了在最坏情况下(所有未被确认的数据均暂未被发送方接收)发送缓存也不会溢出。
然而,当 rwnd 为 0,即接收缓存已满时,发送方仍然会持续发送不含任何有效数据的报文段,以确认接收缓存何时产生了空闲。
设想这样一种情景:接收方发送了一个指示 rwnd 为 0 的报文,发送方接收到该报文后因流量控制不再发送报文。然而由于接收窗口是捎带发送的,若接下来很长一段时间内接收方都不向发送方发送数据,则发送方将一直处于阻塞状态无法发送任何数据。
通过持续发送探测报文,在接收方的应答报文中就会实时更新接收窗口何时有了空余,预防了上面所述情况的发生。
TCP 的流量控制与拥塞控制存在联动。一般而言,发送方发送的未确认的字节数为接收窗口与拥塞窗口二者的小值。
连接管理
TCP 是面向连接的。两个应用程序在发送数据之前需要先发送一些特殊的报文段协商建立连接相关的各种参数,这一个过程被称作 三次握手,指一条 TCP 连接的建立需要至少三个报文段的往来,具体如下:
- 客户端向服务器发出建立连接请求的特殊 TCP 报文。在该报文中,SYN 标志位置位,同时客户端随机选择一个初始序号作为发送方字节流起始序号放在报文中发送。
- 服务器收到客户端的 SYN 报文段,向客户端发送同意连接建立请求的 SYN 报文段。在该报文中,SYN 与 ACK 标志位置位,服务器随机选择一个初始序号作为接收方字节流起始序号放在报文序号字段中;同时确认字段被设置为发送方起始序号加一的值。
- 客户端收到服务器的 SYNACK 报文段,为该连接分配缓存与变量,并向服务器发送最后一个报文段以确认服务器同意建立连接的报文段。由于连接已经建立,该报文 SYN 不置位,且已经可以携带应用层数据,同时该报文的确认序号被设为接收方起始序号加一的值。服务器接收到该报文段后,便为该连接分配缓存与变量,连接正式建立。
服务器不在收到了 SYN 报文后立刻准备连接相关的变量与资源是为了防范 SYN 泛洪攻击,一种经典的 DoS 攻击。在该攻击中,攻击方会向服务器发送大量 SYN 报文段但不完成第三次握手的步骤。如果服务器提前准备好资源就会为大量不会实际建立的半连接浪费资源导致宕机。
当前对于 SYN 洪泛攻击的防御方法是:服务器接收到 SYN 报文后不立刻准备资源,而是根据 SYN 报文段的源地址与目标地址加上保存在服务器本地的秘密数,经一散列函数生成该报文的 TCP 序列号,并将其封装进 SYNACK 分组的序号字段中发送。
如果请求连接的一方是合法的,其将再次用包含相同的源与目标地址、确认号为服务器计算出的 TCP 序列号加一的 ACK 报文回复。服务器用相同的方式检验确认号是否合法,一旦校验通过,服务器才准备对应的资源进行通信。
三次握手避免了两次握手可能造成的只有服务器维护半连接与老的连接的数据对新连接造成影响的问题。
虽然 TCP 连接允许数据双向传输,但是当通信完成需要拆除连接时,一次只会拆除一个方向的连接,如下:
- 客户端向服务器发出连接拆除请求的特殊 TCP 报文。在该报文中,FIN 标志位置位。
- 服务器收到该报文后,回送一个确认报文段,客户端接收到该确认报文后即可回收该连接的缓存与变量。客户端->服务器方向的半连接被拆除。
- 服务器->客户端方向半连接的拆除与上面完全相同。
当然,有时一台主机会收到与某个没有应用程序监听的端口建立连接的请求,此时该主机将发送一个 RST 标志位置位的报文段指示发送方没有报文段对应的套接字。