TCP/IP 协议(10):TCP 协议一百问
TCP/IP 协议(10):TCP 协议一百问
杨领well 的 TCP/IP 协议专栏 TCP 协议部分一直没有更新,是因为我不确定到底应该怎么来介绍 TCP 协议才能干货满满。最后我决定以 Q&A 的形式来介绍 TCP 协议,应该就不会遗漏什么了吧。
P.S. 不要问我为什么 《TCP 协议一百问》 没有一百问。 (> v <)
TCP 协议全称是 传输层控制协议(Transmission Control Protocol), 顾名思义就能知道它是 传输层协议。
TCP 协议一百问
一、为什么大家常说 “TCP 协议是面向连接的,面向字节流的,可依赖的协议”?
- 面向连接的(connection-oriented)协议: 应用程序使用 TCP 协议进行数据交换前,需要先与对端建立 TCP 连接。
我认为,所谓 TCP 连接,就是建立连接的双方知道对端的基本信息并且确认对端能够顺利地进行数据交换。
- 面向字节流的(byte stream) 协议:TCP 协议不知道上层发送数据的格式或类型,对它而言,所有待发送的数据都只是一串字节流,没有起点和终点。
- 可依赖的(reliable)协议:TCP 协议通过以下手段来实现数据交换的可依赖:
(1) 应用程序发送的根据网络环境切割成合适大小的 数据段(Segment)。如果接收方发现数据有误,只需要丢弃一个数据段的数据,降低重传成本。
(2)通过 TCP 协议发送数据之后,会等待对端发送确认信息(ACK),以保证对端已正确接收数据。
(3)TCP 协议维护有 重传计时器(retransmission timer),指定时间内没有收到对端的确认信息,TCP 就会认为该数据段已经丢失。然后,它会重新发送该数据。
(4)TCP 数据段的 校验和(checksum) 字段能从一定程度上保证当前数据段的正确性。
(5)在 TCP 数据段 序列号(sequence number) 的帮助下,TCP 协议能够处理乱序到达的数据。
(6)TCP 协议会丢弃接收到 重复数据(duplicate data)。
(7)TCP 协议提供了详细的 流量控制(flow control) 方式。简单总结就是:分段发送、确认和重传机制、数据段的正确性校验、乱序数据和重复数据的处理以及流量控制。
二、为什么 TCP 协议首部没有数据段总长度字段,而 UDP 协议和 IP 协议有数据报和数据包总长度的字段?
- IP 协议 依赖的下层协议,如 以太网(Ethernet) 协议,可能有最小 帧(frame) 的要求,如果 IP 数据包(packet) 的长度小于最小帧长度,就会填充无意义的数据。因此 IP 数据包的长度不能复用 数据链路层(data link layer) 的数据帧长度,需要数据包首部有长度字段。
- 参考文献[1] 说 UDP 协议 的长度字段是多余的,因为它的 数据报(datagram) 长度是可以由 IP 协议的长度字段计算得出。
我觉得由于 UDP 协议 是 面向事务(transaction oriented) 的,因此它需要将自己的长度保存在首部,方便将可能被拆散的数据还原原本的数据报(毕竟谁也不能保证传输层协议一定总是 IP 协议)。The UDP Length field is redundant; the IP header contains the datagram’s total length. — 参考文献[1]
- 由于 TCP 协议是面向数据流的,因此可以不用担心一个数据段由于传输原因被拆分成几个数据段的问题。
后续几个问题都会涉及到关于 TCP 首部(header) 的问题,因此先在此处贴上 TCP 首部的内容(下图出自 参考文献[2]):
三、URG 标识位和 Urgent Pointer 只能标识紧急数据的结束位置,那么接收端是怎么指定紧急数据的起始位置呢?
- 答案是它并不知道,也不需要知道。
There is no way to specify where the urgent data starts in the data stream. — 参考文献[1]
- 由于 TCP 协议是面向字节流的,因此我们想要快速接收紧急数据,必须先将非紧急数据之前的数据都确认接收才行。
所以 TCP 协议并不在乎紧急数据的起点在哪里,它会认为 Urgent Pointer 之前的数据都是紧急的。 - TCP 接收到 URG 数据段之后,就会将当前状态设为 紧急模式(urgent mode), 在 (Sequence Number + Urgent Pointer) 之前的数据都会被认为是紧急数据处理(包括 Sequence Number 之前还未到达的数据)。
- 紧急数据被处理完之后,TCP 会恢复 普通模式(normal mode) 处理后续到达的数据。
- 题外话:处于紧急模式下的接收端收到新的 URG 数据段,会将紧急数据的结束位置更新。
If the urgent pointer is updated while the user is in “urgent mode”, the update will be invisible to the user. — 参考文献[2]
四、URG 标识位和 PSH 标识位的区别是什么?
- 应用程序通过设置 PSH 标识位来通知 TCP,这些数据需要尽快被发送。对端收到带有 PSH 位的数据段后,会立即将接收到的所有数据发送到应用层。
It(PSH) is a notification from the sender to receiver for the receiver to pass all the data that it has to the receiving process. — 参考文献[1]
- URG 标识位通知对端需要进入紧急模式,优先读取Urgent Pointer 之前的数据(PSH 则没有优先的这层意思,它只是告诉对端:“我发送的数据告一段落,你可以先将前面的数据推给应用层了”)。
五、 为什么 SYN 和 FIN 标识位需要的空数据段需要消耗一个序列号,而 ACK 和 RST 的空数据段则不需要?
因为 SYN 数据段和 FIN 数据段需要被确认(ACK),而 ACK 和 RST 数据段不需要。需要确认的数据段至少需要消耗一个序列号,以便能够确认对端接收到了该数据。
后续几个问题都会涉及到关于 TCP 的状态转换的问题,因此先在此处贴上 TCP 状态转换图(下图出自 参看文献[5]):
六、FIN_WAIT_2 被称为 Half-Close 。如果在该状态下,对端故意一直不发送 FIN 结束连接,是不是本端永远无法结束该状态?
- TCP 协议本身对 Half-Close 没有超时限制。理论上讲对端如果恶意不发送 FIN 关闭连接,该链接将会永远处于 FIN_WAIT_2 状态。
- 应用层非优雅关闭 socket 可以触发 RST 强制结束 FIN_WAIT_2 状态(数据接收缓冲区还有数据,直接
close
。或者 “close + 'l_onoff=1' + 'l_linger=0'
”。更多详情参看 参考文献[7] 图7-12)。 - Linux 通过
/proc/sys/net/ipv4/tcp_fin_timeout
文件来控制的超时时长。
七、TIME_WAIT 状态下接收到对端发送的数据,会做怎样的处理?
- 如果对端发来的是 FIN 数据段,TCP 会向对端发送 ACK 数据段,然后重启 2MSL 的计时。
- 如果对端发来的是非 FIN 数据段,TCP 会直接丢弃掉该数据段,不做任何操作。
八、TIME_WAIT 状态为什么需要等待 2MSL?
- 假设不等待 2MSL 时间,直接将本端设为 CLOSED 状态,而此时对端先接受到了最后一次的 ACK, 然后重新和本端建立一模一样的连接。
这时连接建立之前在网络游走的之前连接的数据传送到了本端,就会被当做是新的连接的数据被错误地解析。因此,需要等待 2MSL 时间来保证之前的连接的数据已经在网络中被丢弃。 - 假设不等待 2MSL 时间,直接将本端设为 CLOSED 状态,而此时最后一次的 ACK 在传输过程中丢失。对端将会重传 FIN,而本端收到 FIN 后发现现在已经没有这个连接了,就会发送 RST,强制重置对端的连接。
If the final ACK from end point 2 is dropped then the end point 1 will resend the final FIN. If the connection had transitioned to CLOSED on end point 2 then the only response possible would be to send an RST as the retransmitted FIN would be unexpected. This would cause end point 1 to receive an error even though all data was transmitted correctly. — 参考文献[6]
九、什么是 Half-Open 连接?如何识别和处理 Half-Open 的连接?(如果对端的电脑崩溃,已建立的连接会一直保持吗?)
- TCP 连接的某端在不通知对端的情况下,单方面的关闭或中断连接,这样的连接被称为 Half-Open 连接。常见的造成 Half-Open 的原因是操作系统崩溃,强制关闭计算机电源等。
下图展示了 Half-Open 出现的状态,出自 参考文献[8]:
- 只要不尝试通过 Half-Open 的连接发送数据,TCP 永远不会知道该连接是 Half-Open 的。这将会造成 TCP 资源的浪费。
As long as there is no attempt to transfer data across a half-open connection, the end that’s still up won’t detect that the other end has crashed. — 参考文献[1]
- 参考文献[7] 第7.5.5 介绍的 SO_KEEPALIVE 选项可以实现对 Half-Open 状态的探测。
设置了该选项后,指定时间内如果没有进行过数据交换,就会进行自动向对端发送 keep-alive probe 数据段:
(1)如果收到 ACK,则表示连接正常,不做任何处理。
(2)如果收到 RST,则表示对端已经不存在该连接,本端也关闭连接。
(3)如果没响应,则会在重试几次后,关闭本端的连接(认为对端不可达,连接已不存在)。Linux 系统在
/proc/sys/net/ipv4
路径下提供了三个文件来控制 keep-alive probe(见 参考文献[9]):- tcp_keepalive_time:指定多久没有进行数据交换后,发送 keep-alive probe。我的系统默认设置的是 7200(s), 即 2 小时内没做数据交换就会进行探测。
- tcp_keepalive_intvl: 指定 keep-alive probe 没响应多久后重试。我的系统默认设置的是 75 (s),即如果没收到探测的 ACK,就会在每 75 s 重新发送探测。
tcp_keepalive_probes: 指定 keep-alive probe 发送几次后会认为对端不可达。我的系统设置的是 9,即如果发送了 9 次探测都没有收到 ACK,就认为对端不可达。
- 应用程序可以通过心跳算法来识别和处理 Half-Open 的连接。如 参考文献[10] 。
十、Half-Open 连接和 Half-Close 连接的区别是什么?
- Half-Close 是 TCP 连接的正常状态,确认本端已没有数据需要发送,等待对端主动发送 FIN,结束连接。
- Half-Open 是一种错误的 TCP 连接,通常是某一段异常结束导致的。
- 从理论上来讲,处于 Half-Close 状态的连接也可能遇到 Half-Open 的情况。
十一、我们知道 TCP 的流量控制(包括拥塞控制)的算法很多,请简述各种算法是为了解决什么样的问题。
流量控制的算法当然是为了解决流量控制的问题咯。(@v@)
- Nagle 算法 和 延迟确认(Delayed Acknowledgment)算法 主要是为了解决小块数据传输问题,避免网络中传输的数据段都是很小的数据段。
(1)Nagle 算法主要从发送方的角度控制小数据段的发送数量。
(2)延迟确认算法主要从接收方的角度控制空 ACK 数据段的发送数量 - 慢启动(Slow Start)算法,拥塞避免(Congestion Avoidance)算法,快速重传(Fast Retransmit)算法 和 快速恢复(Fast Recovery) 算法, 主要是为了解决大块数传输问题。尽可能的利用网络带宽尽最大努力的传输数据,同时避免网络拥塞。
(1)慢启动算法:在连接刚建立或者数据段发送超时的情况下,为了更快的实现更大的传输速率,用该算法来 指数 增加 拥塞窗口(congestion window,cwnd) 的大小。
(2)拥塞避免算法:在拥塞窗口达到 ssthresh(slow start threshold size) 的情况下,为了避免更早的遇到拥塞的情况,用该算法来 线性 增加拥塞窗口的大小。
(3)快速重传算法:当接收到同一数据段的大于或等于三个重复 ACK 时,表示该数据段已经丢失。TCP 不用等待重传计时器到期而直接重传该数据段。这个算法叫做快速重传算法。该算法尽量保证丢失的数据段尽快到达对端。
(4)快速恢复算法:当接收到同一数据段的大于或等于三个重复 ACK 时,表示该数据段已经丢失。TCP 不直接重新开始慢启动,而是使用快速恢复算法将拥塞窗口减小到一个较大的值。这样既保持了一定的传输速度,又降低了网络的压力。
十二、为什么说 Nagle 算法和延迟确认算法一起使用会影响网络性能?
- Nagle 算法在还有发送的数据没被 ACK 之前,尽量不发送小数据段。从而让很多不同时间段发送的小数据段可以合并成大的数据段一次发送,减少网络中小数据段的数量。
- 延迟确认算法会等待一定的时间(如 200 ms),从而让更多的数据一次确认或者在 ACK 数据段中携带后续发送的数据,减少网络中的小数据段(空 ACK 数据段)的数量。
- 当 Nagle 算法遇到延迟确认算法就可能会出现互相等待对方的情况:延迟确认算法想等待尽可能多的数据一次确认,Nagle 算法想等待对端的 ACK 数据段来触发小数据段的发送。因此会导致网络性能的降低。参看 参考文献[11]。
十三、TCP 是怎么感知到 “拥塞控制” 的 “拥塞” 的? 如何处理这些拥塞的?
- TCP 主要有以下渠道知道 拥塞(congestion) 情况:
(1)重传计时器超时。
(2)收到对端对同一个数据段的重复 ACK。
(3)收到 ICMP 的 source quench 数据包。Using the Internet Control Message Protocol (ICMP), a source quench is a message from one host computer to another telling it to reduce the pace at which it is sending packet to that host. — 参考文献[12]
- TCP 通过上述方式感知到拥塞对应的处理算法:
(1)重传计时器超时和收到 source quench 数据包:慢启动算法。
(2)收到重复 ACK:快速恢复和快速重传算法。
十四、为什么处理重传计时器超时用慢启动算法,而处理重复 ACK 用快速恢复算法和快速重传算法?
- 接收到重复的 ACK 说明对端收到了多次后续的数据段,证明端之间的网络连接良好,可能由于网络中传输数据量较大导致网络拥塞。因此可以用快速重传和快速恢复算法在保证传输数据量的情况下,适当减少数据的发送,缓解拥塞。
- 当重传计时器超时,说明了对端根本没收到数据或者发送的 ACK 无法到达本端。可能是网络特别拥塞或者对端已经不可达。因此需要快速的减少数据发送量的慢启动算法来慢慢试探网络状况。
The reason for not performing slow start in this case is that the receipt of the duplicate ACKs tells TCP more than just a packet has been lost. Since the receiver can only generate the duplicate ACK when another segment is received, that segment has left the network and is in the receiver’s buffer. That is, there is still data flowing between the two ends, and TCP does not want to reduce the flow abruptly by going into slow start. — — 参考文献[1]
十五、慢启动算法是在每一次接收到 ACK 数据段后,cwnd 增加 1,可为什么总是说慢启动的 cwnd 的增长是指数级的?
- 以 ACK 接收的数量作为横坐标来看,的确 cwnd 是线性增长的。
- 理想情况下,刚开始,cwnd = 1, 发送方第一次发送一个数据段,等待接收到它的 ACK 数据段。当发送方接收到 ACK 数据段后,cwnd = 2, 发送两个数据段,然后等待这两个数据段的 ACK。当发送方接收到这两个 ACK 数据段后,cwnd = 4,会发送四个数据段,然后等待接收这四个数据段的 ACK。忽略发送数据前的耗时,每个轮次大约花费一个 RTT。因此以时间为单位(或 RTT),cwnd 是指数增长的。
十六、当接收窗口减小为 0 时,会通过 ACK 的 advertised window 来通知发送方,让它先暂停发送数据。那么接收窗口再次打开时,接收方如何通知发送方?如何保证该通知不会丢失?
- 当窗口再次打开时,接收方会发送一个 ACK 数据段更新 advertised window 来通知发送方。
- 由于 ACK 数据段是不可靠的(没有确认机制),因此,当发送方收到 advertised window 为 0 的 ACK 数据段后,会维护一个 persist timer。该计时器定期询问接收方接收窗口是否打开,直到接收窗口打开为止。
如果觉得对您有帮助,欢迎评论,点赞和关注,么么哒!
参考文献
- [ 1 ] TCP/IP详解 卷1:协议(英文版)
- [ 2 ] RFC 793 - Transmission Control Protocol
- [ 3 ] Can anyone Explain me difference Between PSH and URG flag in TCP segment - stackexchange.com
- [ 4 ] Why does a SYN or FIN bit in a TCP segment consume a byte in the sequence number space? - stackoverflow.com
- [ 5 ] TCP/IP State Transition Diagram (RFC793)
- [ 6 ] TIME_WAIT and its design implications for protocols and scalable client server systems
- [ 7 ] UNIX网络编程 卷1:套接字联网API(第3版)
- [ 8 ] TCP keepalive overview - TCP Keepalive HOWTO
- [ 9 ] Using TCP keepalive under Linux - TCP Keepalive HOWTO
- [ 10 ] Detecting Dead TCP Connections with Heartbeats and TCP Keepalives - rabbitmq.com
- [ 11 ] questions about nagle vs. delayed ack - serverfault.com
- [ 12 ] source quench - searchnetworking.techtarget.com