TCP 传输原理

TCP的主要目的是在一个比较不可靠的相互通信上提供可靠的,安全的逻辑回路或者连接服务。为了实现这个服务,TCP 的设计 体系非常庞大,我对其理解只是冰山一角,此文只是将我学过的重点知识进行汇总。

1   连接

国家规定新建小区必须设置信箱,这无形之中就让家家户户都建立起了连接,你想要给任何一家发信件都是可以的,总之连接是抽象的,而信箱是具体的。对应到网络,socket 就是信箱;网络连接是无形的,而 sockets 对各自的机器是具体的数据结构。每个连接被一套指定两端的 sockets 唯一指定,建立连接其实就是双方建立绑定的 socket 的过程,断开连接就是销毁各自的 socket 的过程。当然上面的 socket 是被 accept 的连接 socket 而非主动或被动(倾听)的 socket,因为像 UDP 是主动 socket,是不需要连接的。

1.1   三次握手

TCP 三次握手

  1. 套接字 程序在创建 socket 并 bind 后成为主动套接字,可以用它来进行主动连接,但不能接收连接;进行 listen 时转变为被动 socket(倾听套接字)。 倾听套接字维护两个队列:
  • 未完成连接队列:每个未完成 3 次握手操作的 TCP 连接在这个队列中占有一项,当完成连接将移出该队列,到完成连接队列中;
  • 完成连接队列:每个已经完成 3 次握手操作,但尚未被应用程序接收(调用函数 accept)的 TCP 连接在这个队列中占有一项;当被 accept 接收的时候,将移出该队列,到新建的 socket 中;
  1. 标识
  • ACK:标识表明确认号是否合法,为 0 表示数据报确认号是无效数据。
  • PSH:接收方因此请求数据报一到便可送往应用程序而不必等到缓冲区满时才传送。
  • RST:用于复位由于主机崩溃或其它原因而出现的错误的连接。还可以用于拒绝非法的数据报或者拒绝连接请求。
  1. 选项 可选项大小 0 - 32 byte。下面这些选项一般只在握手阶段设备。如果是抓包分析,错过了握手阶段的包,抓包工具就不知道该如何计算,所以我们有时候会很莫名地看到一些极小的接收窗口值。
  • MSS(0x2): Max Segment Size 是 TCP 报文段里数据的最大长度(不包括 TCP 首部),握手中两端之间选择一个较小的值投入使用,一般是 1460B。原因是 MTU(Max Transmission Unit),由于以太网传输电气方面的限制,每个以太网帧最小为 64B,最大为 1518B,而 Ethernet II 帧最长占用 18B,剩下的 1500B 称为 MTU,它相当于 MSS + TCP 包头(约 20 字节)+IP 包头(20 字节)= 1500 字节;网络层协议如 IP 协议会根据 MTU 这个值来决定是否把上层传下来的数据进行分片。当然网络中不媒介的 MTU 是不同的,本地甚至可以达上万字节,大部分网络 MTU 大于等于 1500。
  • Window Scale (0x3): 窗口比例选项使 TCP 的窗口定义从 16bit(64KB) 增加到 30bit(1GB),通过这个值计算窗口大小值的乘数 Multiplier,比如 Shift count(取值 0 - 14) 值为 6,表示 Multiplier = 2^6。窗口大小等于 Window(TCP 首部) * Multiplier(选项),单位是 octets。TCP 刚被发明的时候,全世界网络带宽很小,所以最大接收窗口被定义为 64KB,随着硬件的革命性进步,64KB 已经成为瓶颈,只能进行扩展。 另外要注意,客户端和服务端的 Window Scale 是各自设置运算互不干扰;这点和 MSS 取两端最小值不同。

1.2   四次挥手

TCP 四次分手

  1. 四次挥手比三次握手多一次的原因就是有一个半关闭状态,关闭的一方只能接收,不能再发送。

  2. 当TCP连接中途突然断掉,使用RST标志位指出连接被异常中止或拒绝连接请求,但是有一点例外,即客户机向服务机发送SYN时,因为服务机倾听套接字backlog已完成连接队列已满时,不会发送RST,有两个原因:

  • 完成队列满的情况是暂时的,客户机应当继续发送几次SYN数据段;
  • 即使发送了客户机也不知道具体是什么原因,只能认为服务机出异常而停止握手;

以下情况会发送RST:

  • 客户端尝试与服务器未对外提供服务的端口建立TCP连接,服务器将会直接向客户端发送reset报文;
  • 客户端和服务器的某一方在交互的过程中发生异常(如程序崩溃等),该方系统将向对端发送TCP reset报文,告之对方释放相关的TCP连接;
  • 接收端收到TCP报文,但是发现该TCP的报文,并不在其已建立的TCP连接列表内,则其直接向对端发送reset报文;
  • 在交互的双方中的某一方长期未收到来自对方的确认报文,则其在超出一定的重传次数或时间后,会主动向对端发送reset报文释放该TCP连接;
  • 有些应用开发者在设计应用系统时,会利用reset报文快速释放已经完成数据交互的TCP连接,以提高业务交互的效率。

2   确认机制 ACK

TCP数据包中的序列号(Sequence Number)不是以报文段来进行编号的,而是将连接生存周期内传输的所有数据当作一个字节流,序列号就是整个字节 流中每个字节的编号。一个TCP数据包中包含多个字节流的数据(即数据段),而且每个TCP数据包中的数据大小不一定相同。在建立TCP连接的三次握手 过程中,通信双方各自已确定了初始的序号x和y,TCP每次传送的报文段中的序号字段值表示所要传送本报文中的第一个字节的序号。

TCP的报文到达确认(ACK),是对接收到的数据的最高序列号的确认,并向发送端返回一个下次接收时期望的TCP数据包的序列号(Ack Number)。例如, 主机A发送的当前数据序号是400,数据长度是100,则接收端收到后会返回一个确认号是501的确认号给主机A。

TCP提供的确认机制,可以在通信过程中可以不对每一个TCP数据包发出单独的确认包(Delayed ACK机制),而是在传送数据时,顺便把确认信息传出, 这样可以大大提高网络的利用率和传输效率。同时,TCP的确认机制,也可以一次确认多个数据报,例如,接收方收到了201,301,401的数据报,则只 需要对401的数据包进行确认即可,对401的数据包的确认也意味着401之前的所有数据包都已经确认,这样也可以提高系统的效率。

如果收到乱序的数据包,那么就应该立即发送 ACK,以确保对方知道重复的确认(DUPACK),然后及时重传。

若发送方在规定时间内没有收到接收方的确认信息,就要将未被确认的数据包重新发送。接收方如果收到一个有差错的报文,则丢弃此报文,并不向发送方 发送确认信息。因此,TCP报文的重传机制是由设置的超时定时器来决定的,在定时的时间内没有收到确认信息,则进行重传。这个定时的时间值的设定非 常重要,太大会使包重传的延时比较大,太小则可能没有来得及收到对方的确认包发送方就再次重传,会使网络陷入无休止的重传过程中。接收方如果收到 了重复的报文,将会丢弃重复的报文,但是必须发回确认信息,否则对方会再次发送。

TCP协议应当保证数据报按序到达接收方。如果接收方收到的数据报文没有错误,只是未按序号,这种现象如何处理呢?TCP协议本身没有规定,而是由TCP 协议的实现者自己去确定。通常有两种方法进行处理:一是对没有按序号到达的报文直接丢弃,二是将未按序号到达的数据包先放于缓冲区内,等待它前面 的序号包到达后,再将它交给应用进程。后一种方法将会提高系统的效率。例如,发送方连续发送了每个报文中100个字节的TCP数据报,其序号分别是1, 101,201,…,701。假如其它7个数据报都收到了,而201这个数据报没有收到,则接收端应当对1和101这两个数据报进行确认,并将数据递交给相关的应用 进程,301至701这5个数据报则应当放于缓冲区,等到201这个数据报到达后,然后按序将201至701这些数据报递交给相关应用进程,并对701数据报进行 确认,确保了应用进程级的TCP数据的按序到达。

3   流量控制 Flow Control

流量控制方法主要有两种:滑动窗口控制和速率控制;TCP 使用前者作为流量控制方式,并且是可变长的滑动窗口。滑动窗口协议定义了在缓存上的窗口,即窗口大小利用缓冲区实现的,但两个是不同的概念。

TCP数据包的TCP头部有一个window字段,它主要是用来告诉对方自己能接收多大的数据(注意只有TCP包中的数据部分占用这个空间),这个字段在通信双方建立连接时协商确定,并且在通信过程中不断更新,故取名为滑动窗口。有了这个字段,数据发送方就知道自己该不该发送数据,以及该发多少数据了。发送方的发送速率高于接收端的接收速率,就需要接收方通过滑动窗口控制发送方的发送速率,保证通信双方的接收缓冲区不会溢出,数据不会丢失。

由于窗口大小在TCP头部只有16位来表示,所以它的最大值是65536,但是对于一些情况来说需要使用更大的滑动窗口,这时候就要使用扩展的滑动窗口,如光纤高速通信网络,或者是卫星长连接网络,需要窗口尽可能的大。这时会使用扩展的32位的滑动窗口大小。参见选项 Window Scale。

3.1   滑动窗口移动规则

  1. 窗口合拢:在收到对端数据后,自己确认了数据的正确性,这些数据会被存储到接收缓冲区,等待应用程序获取。但这时候因为已经确认了数据的正确性,需要向对方发送确认响应ACK,又因为这些数据还没有被应用进程取走,这时候便需要进行窗口合拢,缓冲区的窗口左边缘向右滑动。注意响应的ACK序号是对方发送数据包的序号,一个对方发送的序号,可能因为窗口张开会被响应(ACK)多次。

  2. 窗口张开:窗口收缩后,应用进程一旦从缓冲区(滑动窗口区或接收缓冲区)中取出数据,TCP的滑动窗口需要进行扩张,这时候窗口的右边缘向右扩张,实际上窗口这是一个环形缓冲区,窗口的右边缘扩张会使用原来被应用进程取走内容的缓冲区。在窗口进行扩张后,需要使用ACK通知对端,这时候ACK的序号依然是上次确认收到包的序号。

  3. 窗口收缩,窗口的右边缘向左滑动,称为窗口收缩,HostRequirement RFC强烈建议不要这样做,但TCP必须能够在某一端产生这种情况时进行处理。

3.2   send 行为

默认情况下,send的功能是拷贝指定长度的数据到发送缓冲区,只有当数据被全部拷贝完成后函数才会正确返回,否则进入阻塞状态或等待超时。如果你想修改这种默认行为,将数据直接发送到目标机器,可以将发送缓冲区大小设为0(或通过TCP_NODELAY禁用Nagle算法),这样当send返回时,就表示数据已经正确的、完整的到达了目标机器。注意,这里只表示数据到达目标机器网络缓冲区,并不表示数据已经被对方应用层接收了。

协议层在数据发送过程中,根据对方的滑动窗口,再结合MSS值共同确定TCP报文中数据段的长度,以确保对方接收缓冲区不会溢出。当本方发送缓冲区尚有数据没有发送,而对方滑动窗口已经为0时,协议层将启动探测机制,即每隔一段时间向对方发送一个字节的数据,时间间隔会从刚开始的30s调整为1分钟,最后稳定在2分钟。这个探测机制不仅可以检测到对方滑动窗口是否变化,同时也可以发现对方是否有异常退出的情况。

push标志指示接收端应尽快将数据提交给应用层。如果send函数提交的待发送数据量较小,例如小于1460B(参照MSS值确定),那么协议层会将该报文中的TCP头部的push字段置为1;如果待发送的数据量较大,需要拆成多个数据段发送时,协议层只会将最后一个分段报文的TCP头部的push字段置1。

3.3   recv 行为

默认情况下,recv的功能是从接收缓冲区读取(其实就是拷贝)指定长度的数据。如果将接收缓冲区大小设为0,recv将直接从协议缓冲区(滑动窗口区)读取数据,避免了数据从协议缓冲区到接收缓冲区的拷贝。recv返回的条件有两种:

  1. recv函数传入的应用层接收缓冲区已经读满
  2. 协议层接收到push字段为1的TCP报文,此时recv返回值为实际接收的数据长度

协议层收到TCP数据包后(保存在滑动窗口区),本方的滑动窗口合拢(窗口值减小);当协议层将数据拷贝到接收缓冲区(滑动窗口区—>接收缓冲区),或者应用层调用recv接收数据(接收缓冲区—>应用层缓冲区,滑动窗口区—>应用层缓冲区)后,本方的滑动窗口张开(窗口值增大)。收到数据更新window后,协议层向对方发送ACK确认。

协议层的数据接收动作完全由发送动作驱动,是一个被动行为。在应用层没有任何干涉行为的情况下(比如recv操作等),协议层能够接收并保存的最大数据大小是窗口大小与接收缓冲区大小之和。Windows系统的窗口大小默认是64K,接收缓冲区默认为8K,所以默认情况下协议层最多能够被动接收并保存72K的数据。

4   拥塞控制 Congestion Control

发送方一开始便向网络发送多个报文段,直至达到接收方通告的窗口大小为止,这种方式在局域网是可以的;但是在互联网上链路层复杂,三次握手后,设备无法知道目前的网络状况,如果发送过多的网络包,可能会导致整个网络阻塞且数据被丢弃。所以需要一些机制在面临拥塞时遏制发送方。 发送方始终保持两个窗口:接收方确认窗口和拥塞窗口;取两个窗口的最小值作为可以发送的字节数:实际发送窗口 = min(cwnd, rwnd);当 cwnd > rwnd 的时候是对方的接收能力限制了我的发送速度,而当 rwnd > cwnd 的时候是网络情况造成了发送比较慢。

假设高速链路带宽是 10Gbps,分组大小为 1500 字节(byte),回路延时 RTT 为 100ms,则发送端达到 10Gbps吞吐量时,发送端拥塞窗口大小为 83 333 个分组。

4.1   慢启动算法

TCP 发送端在初始段的发送速率以指数型增长,直至发生超时或者收到冗余的确认报文时,或者达到接收端窗口大小时停止增长。 每个 TCP 连接完成的时候会设置拥塞窗口(cwnd)的大小,拥塞窗口默认大小是 10 个 MSS,这是相对保守的一个值,也就是说完成 TCP 连接后,发送方第一次发送的数据量最多是 10 个 MSS。一旦发送方在定时器超时之前接收到接收方发送的 ACK 包,拥塞窗口大小就增加一倍。当拥塞是 N 个数据报的大小时,如果发送 N 个数据报都被及时确认,那么将拥塞窗口大小增加 N 个数据报对应的字节数目。也就是发送的数据包数量保持指数增长。直到数据传输超时或者达到接收方设定的窗口大小。拥塞窗口便设置为恰好不造成超时或达到接收方的窗口大小的字节数。拥塞窗口这种处理机制称为慢启动,实际上慢启动并不慢。

慢启动的优点是在比较拥塞的网络,慢启动可以避免拥塞进一步加剧,但是它的缺点也是明显的,对于正常的网络,慢启动将降低传输的效率,例如本来一个 RTT 就可以传完的数据,现在要分成几个 RTT;比如 Linux 2 的 initcwnd 只有 3MSS,如果有 7MSS 数据要发送就不得不用 3RTT;如果把 initcwnd 改成 10,则 7MSS 并行发送,只需要 1RTT。 修改某个网卡的 cwnd sudo ip route change default via 127.0.0.1 dev eth0 proto static initcwnd 10; 可以通过命令查看 ss -nli | fgrep cwnd 查看拥塞窗口初始化数据报的数量; 关于 initcwnd 可以查看 Tuning initcwnd for optimum performance

4.2   拥塞避免

  • 拥塞避免:该算法主要用来将 TCP 连接的数据传输速率维持在一个比较接近网络带宽上限,但又不至于引起严重拥塞的水平上。慢启动的过程中,拥塞窗口会以指数倍增长,一直增长到拥塞阈值 ssthresh,然后再以线性递增的方式增加拥塞窗口,这个阶段叫拥塞避免。也就是说当 cwnd < ssthresh 时是慢启动的过程,而当 cwnd > ssthresh 时是拥塞避免。一直增长到合适的带宽大小。

4.3   超时重传

4.4   快速重传和快速恢复

4.5   Nagle 算法

5   定时器管理 Timer

为了实现 TCP,对每个连接 TCP 管理 4 个不同的定时器。

5.1   重传定时器 Retransmession

TCP 提供可靠的传输所使用的方法之一是确认和重传。重传定时器是用于处理重传时间的,TCP 每发送一个报文段后就启动重传定时器;如果超时前收到确认报文,则报文重传定时器被停止;否则该报文将被重传,且定时器被复位。 TCP 采用了一种动态重传时间策略,它能根据往返时间(RTT),不断地调整修正重传时间。RTT 是 TCP 维护的变量,每次进行测量的时候,RTT 将得到修正。

5.2   持续定时器 Persistance

其作用有两个:

  • 零窗口死锁
  • 探测报文段

当接收端向发送端发送一个零窗口报文段(报文段首部中窗口值的大小设置为0),发送端就停止向接收端发送报文段。后来接收端想通知发送端,让其接着发送数据时,便向发送端发送一个非零窗口报文段。该报文段在路上丢失了。而接收端以为该非零报文段已经发送给发送端,而发送端由于没有收到接收方的非零窗口报文段,于是两端都等待,陷入“死锁”的状态。

TCP为发送端设置一个持续计时器,当发送端收到零窗口报文段时,启动该持续计时器,便等待着接收方的非零通知。持续计时器又超时时还没收到,发送端便向接收方发送一个“探测报文段”,该探测报文仅携带1B的数据,该探测报文会消耗一个序号,但特殊的是,该探测报文段的序号永远不需要确认。目的是促使 TCP 接收端重传一个确认,该确认的内容包括接收端希望发送端的发送窗口的大小作为回复,即:如果接收端希望发送端的窗口大小仍然是0,重置续计时器;如果不是0,则该僵局打破了。

Probe ACK 只是对 Probe 的应答,而不是确认序列号,接收端对探测报文段的响应是必须重传确认报文段,并将窗口大小告诉发送方,也就是窗口大小不为零时对探测报文段的序号进行确认。 注意:即便发送发收到了零窗口设置的报文段,发送端也能接收两种报文段:一个字节数据的探测报文段和携带紧急数据的报文段。

我们查看 Wireshark 看探测报文的“专家信息”,其实就是 Probe 和 Probe ACK 两个包在循环:

  1. TCP Zero Window Probe:探测报文段,报文段数据为 1;
  2. ACK to a TCP Zero Window Probe:对 Probe 的应答,但不确认序列号; ……

5.3   保活定时器 keep-alive

保活功能可以保护长连接,也可以让服务端能检测到半开放的连接。这个要注意区别持续定时器,触发启动的方式是不一样的。

  1. TCP keep-alive segment:报文段数据为 0;
  2. ACK to a TCP keep-alive segment:保活应答; ……

5.4   时间等待定时器 time-wait

主动关闭方在关闭连接后处于 TIME_WAIT 状态,启用该定时器。本质原因是 TCP 需要兼顾对服务端和客户端开启和结束地处理,换句话说就是不能只顾一方关闭而不管另一方是否关闭。 1)为了保证A发送的最后一个ACK报文能够到达B。这个ACK报文段有可能丢失,因而使处在LAST-ACK状态的B收不到对已发送的FIN+ACK报文段的确认。B会超时重传这个FIN+ACK报文段,而A就能在2MSL时间内收到这个重传的FIN+ACK报文段。如果A在TIME-WAIT状态不等待一段时间,而是在发送完ACK报文段后就立即释放连接,就无法收到B重传的FIN+ACK报文段,因而也不会再发送一次确认报文段。这样,B就无法按照正常的步骤进入CLOSED状态。 2)A在发送完ACK报文段后,再经过2MSL时间,就可以使本连接持续的时间所产生的所有报文段都从网络中消失。这样就可以使下一个新的连接中不会出现这种旧的连接请求的报文段。

正常在 TIME_WAIT 情况下不能再创建 ip:port 相同的 socket,原因就是上面分析的旧的 socket 滞留的报文段可能给新的 socket 接收了而造成干扰;不过这就导致 2MSL(一般30秒)不能马上重启服务,可以指定 SO_REUSEADDR。

6   设置和调试

6.1   系统参数

linux 和 unix 系统都是使用 sysctl -A 进行查看,有关 tcp 的配置可以使用命令 sysctl -A | grep net.inet.tcp 查看,以下例举一些常用的参数,后台的值是 macOS 系统的默认值。

  • net.inet.tcp.msl: 15000;此时 2msl = 30 秒;
  • net.inet.tcp.keepidle: 7200000;若连接空间2h时间后,保活定时器启动发送探测报文段来查看对方是否还存在;
  • net.inet.tcp.keepintvl: 75000;间隔 75s 发送探测报文
  • net.inet.tcp.keepinit: 75000;
  • net.inet.tcp.keepcnt: 8;探测报文一共发送的次数
  • net.inet.tcp.always_keepalive: 0;

6.2   调试

学习的时候可以自己写服务端和客户端进行,抓包工具可以用 wireshark,内网可以安装插件 npcap。不过建议把服务端部署在外网,这样才能正常模拟网络传输;局域网内抓包,数据干扰因素太多,比如 MSS 设置成了夸张的 16344!TCP Zero Window Probe 那一个字节竟然会被确认!还多出了 TCP Previous segment not captured、TCP ACKed unseen segment 等专家信息提示,很容易逻辑混淆。

下面是我用 C 语言实现的客户端和服务端,并把服务端分别部署在外网和本地进行抓包: tcp 服务端 C 语言实现tcp 客户端 C 语言实现 wireshark 外网抓包分析wireshark 本地抓包分析

变更窗口由算法运算,通过 socket 状态可以看一下何时变更窗口;处于发送探测包的时候 socket 形式如下:

Proto Recv-Q Send-Q  Local Address          Foreign Address        (state)
tcp4  560949      0  127.0.0.1.8090         127.0.0.1.64196        ESTABLISHED
tcp4       0  22243  127.0.0.1.64196        127.0.0.1.8090         FIN_WAIT_1

8090 端是读取,64196 端是发送,当 8090 端的接收窗口为零,持续定时器被启用,第一行的值一直在减少(读取),第二行的值保持不变。 当 8090 端剩下 131404 字节时更新窗口大小为 26220 字节,于是马上变成:

Proto Recv-Q Send-Q  Local Address          Foreign Address        (state)
tcp4  152384      0  127.0.0.1.8090         127.0.0.1.64196        CLOSE_WAIT
tcp4       0      0  127.0.0.1.64196        127.0.0.1.8090         FIN_WAIT_2

除此之外,本系列 HTTPS 一文有提供更多的抓包调试工具。

参考文献 [1] 杨延双.等. TCP/IP 协议分析及应用. 版次:2010年1月第1版 [2] 忆常. TCP报文到达确认(ACK)机制. https://blog.csdn.net/wjtxt/article/details/6606022 [3] 徐永士.等. 大数据时代下的通信需求——TCP传输原理与优化. 版次:2015年8月第1版 [4] 李银城. 高效前端 Web高效编程与优化实践. 版次:2018年3月第1版 [5] TCP 性能优化详解. https://www.zhuxiaodong.net/2018/tcp-performance-optimize-instruction/ [6] RFC793 TCP标准 最初的TCP标准定义,但不包括TCP相关操作细节 RFC813 TCP窗口与确认策略 讨论窗口确认机制,以及描述了在使用该机制存在的问题及解决方法 RFC879 TCP最大分段大小及相关主题 讨论MSS参数在控制TCP分组大小的重要性,以及该参数与IP分段大小的关系等 RFC896 IP/TCP网络互联拥塞控制 探讨拥塞问题与TCP如何控制拥塞 RFC2525 已知TCP的问题 描述当前已知的部分TCP问题 RFC2581 TCP拥塞控制 描述用于拥塞控制的四种机制:慢启动、拥塞防御、快重传和快恢复 RFC2988 TCP重传计时器计算 讨论与TCP重传计时器设置相关话题,重传计时器控制报文在重传前应等待多长时间