TCP:学得越多越不懂

2020-04-06 11:35:43 +08:00
 felix021

周末小课堂又开张了,这次我们来聊一聊 TCP 协议。

== 握手 ==

多少有点令人意外的是,大多数程序员对 TCP 协议的印象仅限于在创建连接时的三次握手。

严格地说,“三次握手”其实是一个不太准确的翻译,英文原文是 "3-way handshake",意思是握手有三个步骤。

不过既然教科书都这么翻译,我就只能先忍了。

“三次握手”的步骤相信各位都非常熟悉了:

A: 喂,听得到吗 (SYN)
B: 阔以,你呢 (SYN-ACK)
A: 我也阔以,开始唠吧 (ACK)

(咦,这不是远程面试的开场白吗)

那么问题来了:为什么不是 2 次握手或者 4 次握手呢?

== 3 次 ==

针对“为什么不是 4 次”,知乎的段子手是这么回答的:

A: 喂,听得到吗 (SYN)
B: 阔以,你呢 (SYN-ACK)
A: 我也阔以,你呢 (SYN-ACK)
B: ...我不想和傻\*说话 (FIN)

<s>由此可见知乎质量的下降。</s>

实际上,上面省略了真正重要的信息,在握手过程中传输的,不是“你能不能听得到”,而是:

A: 喂,我的数据从 x 开始编号 (SYN)
B: 知道了,我的从 y 开始编号 (SYN-ACK)
A: 行,咱俩开始唠吧 (ACK)

协商一个序号的过程需要一个来回(告知 + 确认),理论上需要 2 个来回( 4 次),互相确认了双方的初始序号( ISN,Initial Sequence Number ),才能真正开始通信。

由于第二个来回的“告知”可以和前一次的“确认”合并在同一个报文里(具体怎么结合后面讲),因此最终只需要 3 次握手,就可以建立起一个 tcp 链接。

这也解释了为什么不能只有 2 次握手:因为只能协商一个序号。

不过话说回来,知乎段子手的回复也不是全在抖机灵:毕竟,发起方怎么才能确认接收方已经知道发起方知道接收方知道了呢?即使发起方再问一遍,接收方又怎么知道发起方知道了接收方知道了呢?

很遗憾,结论是:无论多少个来回都不能保证双方达成一致。

由于实践中丢包率通常不高,因此最合理的做法就是 3 次握手( 2 个来回),少了不够,多了白搭;同时配上相应的容错机制。

例如 SYN+ACK 包丢失,那么发起方在等待超时后重传 SYN 包即可。

想想看,如果最后一个 ACK 丢了会怎样?

然后问题又来了:为什么需要协商初始序号,才能开始通信呢?

== 可靠 ==

我们都知道,tcp 是一个“可靠”( Reliable )的协议。

这里“可靠”指的不是保证送达,毕竟网络链路中存在太多不可靠因素。

在 IETF 的 RFC 793 ( TCP 协议)中,Reliability 的具体定义是:TCP 协议必须能够应对网络通信系统中损坏、丢失、重复或者乱序发送的数据。

Reliability:

The TCP must recover from data that is damaged, lost, duplicated, or delivered out of order by the internet communication system.

https://tools.ietf.org/html/rfc793

为了保证这一点,tcp 需要给每一个 [字节] 编号:双方通过三次握手,互相确定了对方的初始序号,后续 [每个包的序号 - 初始序号] 就能标识该包在字节流中所处的位置,这样就可以通过重传来保证数据的连续性。

举个例子:

由于接收方没有收到 4003,因此给发送方的 ACK 中,序号最大值是 4003 (表示收到了 4003 之前的数据)。

过了一段时间( Linux 下默认是 1s ),发送方发现 4003 一直没被 ACK,就会重传这个包。

当接收方最终收到 4003 以后,上层应用才可以读到 4003 和 4004,从而保证其收到的消息都是可靠的。(以及,接收方需要给发送方 ACK,序号是 4005 )

注意:虽然 ISN=4000,但是发送方发送的第一个包,SEQ 是 4001 开始的,TCP 协议规定 SYN 需要占一个序号(虽然 SYN 并不是实际传输的数据),所以前面示意图中 ACK 的 seq 是 x+1 。同样,FIN 也会占用一个序号,这样可以保证 FIN 报文的重传和确认不会有歧义。

但是,为什么序号不能从 0 开始呢?

== 可靠² ==

真实世界的复杂性总是让人头秃。

我们知道,操作系统使用五元组(协议=tcp,源 IP,源端口,目的 IP,目的端口)来标识一个连接,当一个包抵达时,会根据这个包的信息,将它分发到对应的连接去处理。

一般情况下,服务器的端口号通常是固定的(如 http 80 ),而操作系统会为客户端随机分配一个最近没有被使用的端口号,因此包总能被分发到正确的连接里。

但在某些特殊的场景下(例如快速、连续地开启和关闭连接),客户端使用的端口号也可能和上一次一样(或者用了其他刚断开的连接的端口号)。

而 TCP 协议并不对此作出限制:

The protocol places no restriction on a particular connection being used over and over again. ... New instances of a connection will be referred to as incarnations of the connection.

那么:

因此我们需要精心挑选一个 ISN,使得上述 case 发生的可能性尽可能低。

注意:不是在 tcp 协议的层面上 100%避免,因为这会导致协议变得更复杂,实现上增加额外的开销,而在绝大多数情况下是不必要的。如果需要“100%可靠”,需要在应用层协议上增加额外的校验机制;或者使用类似 IPSec 这样的网络层协议来保证对包的有效识别。

那么,ISN 应该如何挑选呢?

== ISN 生成器 ==

说起来其实很简单:

TCP 协议的要求是,实现一个大约每 4 微秒加 1 的 32bit 计数器(时钟),在每次创建一个新连接时,使用这个计数器的值作为 ISN 。

假设传输速度是 2 Mb/s,连接使用的 sequence number 大约需要 4.55 小时才会溢出并绕回( wrap-around )到 ISN 。即使提高到 100 Mb/s,也需要大约 5.4 分钟。

而一个包在网络中滞留的时间通常是有限的,这个时间我们称之为 MSL ( Maximum Segment Lifetime ),工程实践中一般认为不会超过 2 分钟。

所以我们一般不用担心本次连接的早期 segment ( tcp 协议称之为 old duplicates )导致的混淆。

注:在家用千兆以太网已经逐渐普及、服务器间开始使用万兆以太网卡的今天,wrap-around 的时间已经降低到 32.8s (千兆)、3.28s (万兆),这个假定已经不太站得住脚了,因此 rfc1185 针对这种高带宽环境提出了一种扩展方案,通过在报文中加上时间戳,从而可以识别出这些 old duplicates 。

主要风险在于前面提到的场景:前一个连接可能传输了较多数据,因此其序列号可能大于当前连接的 ISN ;如果该连接的报文因为某种原因滞留、现在又突然冒出来,当前连接将无法分辨。

因此,TCP 协议要求在断开连接时,TIME-WAIT 状态需要保留 2 MSL 的时间才能转成 CLOSED (如下图底部所示)。

                              +---------+ ---------\      active OPEN
                              |  CLOSED |            \    -----------
                              +---------+<---------\   \   create TCB
                                |     ^              \   \  snd SYN
                   passive OPEN |     |   CLOSE        \   \
                   ------------ |     | ----------       \   \
                    create TCB  |     | delete TCB         \   \
                                V     |                      \   \
                              +---------+            CLOSE    |    \
                              |  LISTEN |          ---------- |     |
                              +---------+          delete TCB |     |
                   rcv SYN      |     |     SEND              |     |
                  -----------   |     |    -------            |     V
 +---------+      snd SYN,ACK  /       \   snd SYN          +---------+
 |         |<-----------------           ------------------>|         |
 |   SYN   |                    rcv SYN                     |   SYN   |
 |   RCVD  |<-----------------------------------------------|   SENT  |
 |         |                    snd ACK                     |         |
 |         |------------------           -------------------|         |
 +---------+   rcv ACK of SYN  \       /  rcv SYN,ACK       +---------+
   |           --------------   |     |   -----------
   |                  x         |     |     snd ACK
   |                            V     V
   |  CLOSE                   +---------+
   | -------                  |  ESTAB  |
   | snd FIN                  +---------+
   |                   CLOSE    |     |    rcv FIN
   V                  -------   |     |    -------
 +---------+          snd FIN  /       \   snd ACK          +---------+
 |  FIN    |<-----------------           ------------------>|  CLOSE  |
 | WAIT-1  |------------------                              |   WAIT  |
 +---------+          rcv FIN  \                            +---------+
   | rcv ACK of FIN   -------   |                            CLOSE  |
   | --------------   snd ACK   |                           ------- |
   V        x                   V                           snd FIN V
 +---------+                  +---------+                   +---------+
 |FINWAIT-2|                  | CLOSING |                   | LAST-ACK|
 +---------+                  +---------+                   +---------+
   |                rcv ACK of FIN |                 rcv ACK of FIN |
   |  rcv FIN       -------------- |    Timeout=2MSL -------------- |
   |  -------              x       V    ------------        x       V
    \ snd ACK                 +---------+delete TCB         +---------+
     ------------------------>|TIME WAIT|------------------>| CLOSED  |
                              +---------+                   +---------+

                      TCP Connection State Diagram
                               Figure 6.

( tcp 连接状态图,截取自 rfc 793 )

那么问题又来了:为什么只有 TIME-WAIT 需要等待 2MSL,而 LAST-ACK 不需要呢?

== 报文 ==

针对 TCP 协议可以提的问题太多了,写得有点累,所以这里不打算继续自问自答了。

但写了这么多,还没有看一下 TCP 报文是什么结构的,实在不应该,这里还是祭出 rfc 793 里的 ascii art (并顺便佩服 rfc 大佬的画图功力)

    0                   1                   2                   3
    0 1 2 3 4 5 6 7 8 9 0 1 2 3 4 5 6 7 8 9 0 1 2 3 4 5 6 7 8 9 0 1
   +-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+
   |          Source Port          |       Destination Port        |
   +-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+
   |                        Sequence Number                        |
   +-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+
   |                    Acknowledgment Number                      |
   +-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+
   |  Data |           |U|A|P|R|S|F|                               |
   | Offset| Reserved  |R|C|S|S|Y|I|            Window             |
   |       |           |G|K|H|T|N|N|                               |
   +-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+
   |           Checksum            |         Urgent Pointer        |
   +-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+
   |                    Options                    |    Padding    |
   +-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+
   |                             data                              |
   +-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+

                            TCP Header Format

简单介绍下:

举个例子,三次握手的第二步,SYN 和 ACK 合并的报文就是这么生成的:

写不动了,真是没完没了(相信看到这里的同学已经不多了),但是 TCP 协议中还有很多有意思的设计本文完全没有涉及,文末我给出一些推荐阅读的链接,供感兴趣的同学参考。

== 总结 ==

~ 投递链接 ~

后端开发(上海) https://job.toutiao.com/s/sBAvKe

后端开发(北京) https://job.toutiao.com/s/sBMyxk

广告策略研发(上海) https://job.toutiao.com/s/sBDMAK

其他地区、职能线 https://job.toutiao.com/s/sB9Jqk

== 推荐阅读 ==

[1] RFC 793:TRANSMISSION CONTROL PROTOCOL

https://tools.ietf.org/html/rfc793

[2] Coolshell - TCP 的那些事儿 (上 & 下)

https://coolshell.cn/articles/11564.html

https://coolshell.cn/articles/11609.html

[3] 知乎 - TCP 为什么是三次握手,而不是两次或四?

https://www.zhihu.com/question/24853633

10122 次点击
所在节点    程序员
34 条回复
felix021
2020-04-06 11:45:05 +08:00
回答文中的问题:三次握手中的最后一个 ACK 丢了会怎样?

A:如果接着有数据要发(例如 http 请求),由于在 A 看来连接已经建立,可以立即发出下一个数据包,这个报文中也会有 ack seq,B 收到后就能成功建立连接。

B:如果一直没收到 ACK,SYN+ACK 这个包的计时器会超时,然后主动重传,直到收到 A 的 ACK ;如果重传多次仍然没有收到 ACK,则停止尝试,关闭该 TCP 连接。
felix021
2020-04-06 15:24:50 +08:00
827 次点击,627 个会员阅读,21 个收藏,1 个感谢,0 个回复

_(:-] 」∠)_
Liampor
2020-04-06 15:40:47 +08:00
赞,就是那种字符画是用什么画的呢
LosLord
2020-04-06 16:01:24 +08:00
现在招人都这么花式的吗
also24
2020-04-06 16:22:52 +08:00
感谢楼主持续分享,正巧最近在看一本书风格很不错,顺道推荐一下:
《 Wireshark 网络分析就这么简单》
https://book.douban.com/subject/26268767/


很多人没有理解三次握手,我觉得其实是有个小地方被忽略了:

A: 喂,我的数据从 x 开始编号 (SYN)( seq-A = x )
B: 知道了,我的从 y 开始编号,我知道你从 x 开始编号了 (SYN-ACK)( seq-B = y, ack-A = x+1 )
A: 行,我知道你从 y 开始编号了,咱俩开始唠吧 (ACK)( seq-A = x+1, ack-B = y+1 )

x 序列的 seq ack 和 y 序列的 seq ack 其实是两条线,分开来看更清晰。
很多书本或者教程里在这个部分写的有点模糊,就让一些人混淆了。


另:
关于 seq num 的选择问题,rfc793 确实提出了 4 微秒变化的方案。
但是在 rfc1948 中提出了针对 seq num 进行猜测攻击的问题。
并在 rfc6528 中提出了新的 seq num 生成算法用于替换 RFC793 中的方案。

在 rfc7414 可以查阅到更多关于 TCP 协议的变动。

参考链接:
https://book.douban.com/subject/26268767/
https://tools.ietf.org/html/rfc793
https://tools.ietf.org/html/rfc1948
https://tools.ietf.org/html/rfc5961
https://tools.ietf.org/html/rfc6528
https://tools.ietf.org/html/rfc7414
hcocoa
2020-04-06 16:35:12 +08:00
看题目以为要讨论拥塞控制
lhx2008
2020-04-06 16:57:43 +08:00
确实的干货,知乎上面的那个回答完全就是瞎搞
SnoopyCat
2020-04-06 17:00:15 +08:00
支持支持
fishioon
2020-04-06 17:26:40 +08:00
优秀的文章;其实某种意义上 TCP 是支持 2 次握手的,TCP 握手第三步是客户端发 ACK 给服务端,假如这时候不发送 ACK,直接发 DATA+ACK,服务也是能够正常转换成 established 并且接受数据的
dexter
2020-04-06 18:47:57 +08:00
收藏收藏,浅显易懂,像我这样的小白都看的懂了
ujued
2020-04-06 18:50:36 +08:00
1. 注意:不是在 tcp 协议的层面上 100%避免,因为这会导致协议变得更复杂,实现上增加额外的开销,而在绝大多数情况下是不必要的。如果需要“100%可靠”,需要在应用层协议上增加额外的校验机制;或者使用类似 IPSec 这样的网络层协议来保证对包的有效识别。

不是太理解。TCP 是可以 100%准确传递上层要求传递的数据分组的,收到 ACK 分组,才算分组传递成功,这是有硬性要求分组准确有序传达。

2. TCP“三次握手”翻译不准确

三次握手这个还算准确,毕竟礼仪之邦!
3 次数据分组的发送,可以理解为 3 种方式握手,即双方相互握手。
说是 一次握手分 3 步 问题也不大。


3. 序列号是用于保证通信的可靠性

更准确的讲,序列号是为了解决 ACK 分组受损或丢失而超时重发哪些分组,以及分组排序(选择重传差错恢复策略)。通信的可靠性不仅仅是由序列号保证的,是和检验和、ACK 分组、重传机制一起协作完成的。


4. 不使用 0 作为 ISN 可以避免一些坑

可以用 0 作为 ISN,每个 TCP 连接 ISN 起始号是随时间递增取模再加一个 hash 的值,可以为 0 。不过不会固定使用 0 或任何可以猜到的数字作为起始号是为了安全考虑。RFC1948 有详细介绍,以及为什么在 TCP 协议解决,如何解决的。大致问题是这样的:
如果在 3 次握手中,攻击者在发起连接的一方未收到 ACK 时而率先发送自己的 ISN,而抢先和对方建立连接,这时 3 次握手成了非法入侵者的机会。
also24
2020-04-06 19:13:12 +08:00
@ujued #11
1 、联系上下文来看,楼主说的 『 100%避免』指的应该是:
精心挑选的 ISN 无法 100% 的避免『误收旧包』和『恶意伪造』这两种情况。

2 、这个全看个人喜好,我赞同『三次』更容易带来误解

3 、其实我感觉楼主这里应该没有将它描述为充分条件的意思,如果描述为这样可能会更严谨:
TCP 设计了若干基于序列号的机制,用于保证通信的可靠性。


4 、联系上下文来看,楼主所说的『不使用 0 作为 ISN 』应该指的是:
『不固定使用 0 』 作为 ISN 。而不是说 ISN 『始终不应为 0 』。


另外,『不固定使用 0 』最初应该只是为了避免 『误收旧包』。
后续 rfc1948 提出的『不使用容易被猜到的数字』才是为了安全原因。

这一点可以在 rfc793 中查证:
To avoid confusion we must prevent segments from one incarnation of a connection from being used while the same sequence numbers may still be present in the network from an earlier incarnation. We want to assure this, even if a TCP crashes and loses all knowledge of the sequence numbers it has been using. When new connections are created, an initial sequence number (ISN) generator is employed which selects a new 32 bit ISN. The generator is bound to a (possibly fictitious) 32 bit clock whose low order bit is incremented roughly every 4 microseconds. Thus, the ISN cycles approximately every 4.55 hours. Since we assume that segments will stay in the network no more than the Maximum Segment Lifetime (MSL) and that the MSL is less than 4.55 hours we can reasonably assume that ISN's will be unique.
ujued
2020-04-06 19:29:19 +08:00
@also24 ❤️
AllenHua
2020-04-06 19:35:54 +08:00
你收藏有了
felix021
2020-04-06 19:39:36 +08:00
@Liampor 我猜是 vim 或者 emacs
felix021
2020-04-06 19:39:58 +08:00
@LosLord 招人太难啊。
felix021
2020-04-06 19:46:07 +08:00
@hcocoa 本来想写一点的,但开了个头就写了 3000 多字,实在写不动了
felix021
2020-04-06 19:50:10 +08:00
@fishioon 对的,这点我在第一个回复里提到了。实际上对于 A 来说,多发一个 ack 没啥区别,因为不用等 B 的回复,数据可以立即发出; 但对于 B 来说,没有 ack 就没法继续,这头看起来还是 3 次。
fishioon
2020-04-06 21:25:57 +08:00
@Liampor 现在有 http://asciiflow.com 来画这种图了
felix021
2020-04-06 21:29:49 +08:00
@fishioon 这个厉害了

这是一个专为移动设备优化的页面(即为了让你能够在 Google 搜索结果里秒开这个页面),如果你希望参与 V2EX 社区的讨论,你可以继续到 V2EX 上打开本讨论主题的完整版本。

https://www.v2ex.com/t/659779

V2EX 是创意工作者们的社区,是一个分享自己正在做的有趣事物、交流想法,可以遇见新朋友甚至新机会的地方。

V2EX is a community of developers, designers and creative people.

© 2021 V2EX