5

TCP:学得越多越不懂

 3 years ago
source link: https://mp.weixin.qq.com/s/xyPUEFUr_v9sSKKqlBkI7w
Go to the source link to view the article. You can view the picture content, updated content and better typesetting reading experience. If the link is broken, please click the button below to view the snapshot at that time.
neoserver,ios ssh client

TCP:学得越多越不懂

Original felix021 felix021 2020-04-06
640?wx_fmt=png

本文一共3366字,要读多久我也不知道,反正我写了好久

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


== 握手 ==

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

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

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

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

640?wx_fmt=png

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

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


== 3次 ==

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

640?wx_fmt=png

由此可见知乎质量的下降。

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

640?wx_fmt=png

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

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

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

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

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

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

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

640?wx_fmt=jpeg

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

举个例子:

  • 发送方(ISN=4000)

    • 发出 4001、4002、4003、4004

    • (假设每个包只有1字节的数据)

    • 收到 4001、4002、4004

    • 4003因为某种原因没有抵达

    • 这时上层应用只能读到4001、4002中的信息

由于接收方没有收到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 开始呢?


== 可靠² ==

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

640?wx_fmt=jpeg

我们知道,操作系统使用五元组(协议=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.

  • 如果前一个连接的包,因为某种原因滞留在网络中,这会儿才送达,客户端可能无法区分(其sequence number在本连接中可能是有效的)。

  • 恶意第三方伪造报文的难度很小。注意,在这个场景里,第三方并 [不需要] 处于通信双方的链路之间,只要他发出的报文可以抵达通信的一方即可。

因此我们需要精心挑选一个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(如下图底部所示)。

640?wx_fmt=png

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

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


== 报文 ==

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

640?wx_fmt=png

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

640?wx_fmt=png

简单介绍下:

  1. 一行是4个字节(32 bits),header一般共5行(options和padding是可选的)

  2. 第一行包含了源端口和目的端口

    1. 每个端口16bits,所以端口最大是65535

    2. 源IP和目的IP在IP报文头里

  3. 第二行是本次报文的Sequence Number

  4. 第三行是ACK序列号

  5. 第四行包含了较多信息:

    1. 数据偏移量:4字节的倍数,最小是0101(5),表示数据从第20个字节开始(大部分情况)

    2. 控制位(CTL):一共6个,其中的ACK、SYN、FIN就不介绍了

    3. RST是Reset,遇到异常情况时通知对方重置连接(我们敬爱的防火墙很爱用它)

    4. URG表示这个报文很重要,应该优先传送、接收方应该及时给上层应用。URG的数据不影响seq,实际很少被用到,感兴趣的话可以参考下RFC 854(Telnet协议)

    5. PSH表示这个报文不应该被缓存、应当立即被发送出去。在交互式应用中比较常用,如ssh,用户每按下一个键都应该及时发出去。注意和Nagle算法可能会有一些冲突。

    6. 窗口大小:表示这个包的发送方当前可以接受的数据量(字节数),从这个包里的ack序号开始算起。用于控制滑动窗口大小的关键字段就是它了。

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

  • Sequence Number填入从ISN生成器中获取的值

  • Acknowledgement Number填入 [发送方的序号 + 1]

  • 将控制位中的ACK位、SYN位都置1

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


== 总结 ==

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

  2. 握手的目的是双方协商初始序列号ISN

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

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

  5. TCP报文里包含了端口号、2个序列号、一些控制位、滑动窗口大小

  6. 我在字节跳动网盟广告业务线(穿山甲),由于业务持续高速发展,长期缺人。关于字节跳动面试的详情,可参考我之前写的《程序员面试指北:面试官视角

~ 投递链接 ~

后端开发(上海)

https://job.toutiao.com/s/sBAvKe

640?wx_fmt=png

后端开发(北京)

https://job.toutiao.com/s/sBMyxk

640?wx_fmt=png

广告策略研发(上海)

https://job.toutiao.com/s/sBDMAK

640?wx_fmt=png

其他地区、职能线

https://job.toutiao.com/s/sB9Jqk

640?wx_fmt=png


[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


About Joyk


Aggregate valuable and interesting links.
Joyk means Joy of geeK