vlambda博客
学习文章列表

16、TCP协议之三次握手

    通过上一篇的学习,我们了解到传输层中的UDP协议是一种无连接的协议,不在乎数据有没有被对方收到,因此很高效快捷,但是不可靠,可靠性实际上是更加重要的,这也是TCP应用更加广泛的原因,相应的代价就是TCP的设计要比UDP复杂得很多,唉,鱼与熊掌不可兼得吗?

    不过反过来想,没有技术壁垒的东西,大家一学就会,就不会有差距,那么对TCP的理解或许就是计算机网络世界中定位青铜还是王者的重要区别。还是有理由好好学习它。我们暂时先不要深究,先从经典的三次握手来个入门吧!前方高能,坐稳扶牢,开始发车!

16、TCP协议之三次握手

一、交流之前先建立通信

    在 TCP 的世界里,对于发送的数据进行确认是它的最大特色,这样才能保证你发的我真的收到了,其实这种确认从第一步连接就开始了!

  • fossi 进行拨号,嘟嘟嘟,等待 stephen 接听,接听成功,先喊一声:hello

  • fossi 说:hello,我是你爸爸 fossi 啊!

  • stephen 说:奥,原来是儿子 fossi 啊,啥事啊?

  • fossi 巴拉巴拉...

    也就是说,在正式巴拉巴拉之前,有一个建立通信的过程,不然对方有没有在听你说话都不能确定呢!这个过程对于TCP也是一样的道理。我们在发送正式数据之前,我们要先建立通信,就是发“hello”的过程。TCP的过程:

  • 你想和我聊天吗?

  • 是的,我已经准备好了!

  • 好的收到,让我们开始聊天吧。

    因此需要某些信息来表明此数据是一个连接请求(对应hello)?响应/确认?(对应好的/收到),这个就是由 TCP 头上的标识位来做区分的。

二、三次握手

下图展示的是 TCP 首部:

16、TCP协议之三次握手

    字段比较多,有颜色的是本文最需要关注的:序列号、确认号、ACK、RST、SYN、FIN。最后四个就是上面说的标识位,这四位很重要,标识了 TCP 请求的类型,先来看下他们分别的作用:

  • ACK:该位为 1 时,「确认应答」的字段变为有效,TCP 规定除了最初建立连接时的 SYN 包之外该位必须设置为 1 。

  • RST:该位为 1 时,表示 TCP 连接中出现异常必须强制断开连接。

  • SYC:该位为 1 时,表示希望建立连接,并在其「序列号」的字段进行序列号初始值的设定。

  • FIN:该位为 1 时,表示今后不会再有数据发送,希望断开连接。当通信结束希望断开连接时,通信双方的主机之间就可以相互交换 FIN 位置为 1 的 TCP 段。

三次握手的一般过程如图:

16、TCP协议之三次握手

  • fossi:hello,我想找你通话,你愿意不?为了确保你本次通话有效,我现在说下我的暗号是1,你小子收到请回复我2

  • stephen:我收到了,我愿意!我给你回复2,另外我也有暗号,我的暗号是99,你小子愿意跟我聊天的话请回复100给我

  • fossi:收到收到,我给你100!好兄弟,下面我们开始说点秘密事!

(这里的暗号比喻不是很好,这里说的暗号其实是 ISN ,下面会细说,这个字段很重要,含义比我说的暗号要深多了,作用也大多了,因此举例跟实际阐述还是有差别的,例子只是引子)下面来好好看下 TCP 中双方是如何三次握手的:

①第一步,客户端发 SYN 请求连接:

16、TCP协议之三次握手

注意,这里还涉及到 ISN ,即初始序列号。

在建立连接时由计算机生成的随机数作为其初始值,通过 SYN 包传给接收端主机,每发送一次数据,就「累加」一次该「数据字节数」的大小。用来解决网络包乱序问题。

②第二步,服务端发 ACK 和 SYN :

16、TCP协议之三次握手

    服务端接收到 SYN 连接请求后,通常是会回复同意与客户端应用程序进行通信,因为如果不同意就没有后续了。同意的这个场景下,服务端会回复 ACK 标志位作为响应。这里涉及到确认号:

指下一次「期望」收到的数据的序列号,发送端收到这个确认应答以后可以认为在这个序号以前的数据都已经被正常接收。用来解决不丢包的问题。

    此外要注意,务器应用程序还会询问客户端是否要与自己通信,因此除了 ACK 标志位以外,也要设置 SYN 标志位。因此,将在其响应中设置 SYN + ACK 标志位(就是 SYN 和 ACK 这两个标志位都设置为 1)

    这里引申出一个重要问题:如果客户端要求与服务器通信,服务器为什么还要问它是否要与自己通信呢?这不是多此一举吗?

事实上,当你想使用 TCP 进行通信时,你不是建立一个连接,而是建立两个连接!

TCP 认为在一个方向上有一个通信,在另一个方向也有一个通信。因此,它为每个通信方向建立一个连接。所以说 TCP 是全双工的。

前两步都是不带任何应用层数据的,也不允许带数据。下面来到最后一步:

③第三步,服务端也要知道客户端愿不愿意跟他通信,因此客户端将发送带有 ACK 标志位的数据包。

不难想象:前面两次握手时不能携带数据的,不过第三次握手时可以携带数据的。

三次握手简化图就是:

16、TCP协议之三次握手

用 wireshark 实际抓了一个包看了下三次握手,观察是否符合上述三个过程:

16、TCP协议之三次握手

    可以看到,还有其他的重要信息,暂且先不管。我们要知道初始序列号是随机生成的,并不是从0开始的,所以不要被上图所示的值误导。ISN 不能被设置为固定值,处于安全性和避免前后连接互相干扰的考虑。

三、三次握手的状态变化

16、TCP协议之三次握手

整个过程为:

  • 第一步:客户端发送连接请求,进入 SYN-SENT 状态,等到服务端 ACK。

  • 第二步:服务端一开始处于监听状态,收到客户端连接请求后,回复 ACK + SYN 后进入 SYN-RCVD 等待客户端 ACK。

  • 第三步:客户端在 SYN-SENT 状态下收到服务端的 ACK 后,进入 ESTABLISHED 状态,并回复 ACK 给服务端。服务端收到客户端确认报文后,也进入 ESTABLISHED 状态。

  • 双方互发数据。

    如果客户端发了 SYN 数据包,迟迟没有收到服务端的 ACK ,则客户端会进行定时重发多次 SYN 包。多次重试后还是无效,则放弃重试,如果在JAVA语言中会返回java.net.ConnectException: Connection timed out异常。

    若一切正常,双方都处于 ESTABLISHED 状态,此连接就已建立完成,客户端和服务端就可以相互发送数据了。在Linux系统下可以通过 netstat -napt 命令查看 TCP 连接状态:

16、TCP协议之三次握手

四、RST报文

    RST 是 reset 的缩写,表示 “重置”,所以 RST 标志位用于重置连接。在 TCP 中发送的每个数据都必须被确认。如果发送的数据和接收的数据之间存在不一致,则该连接就被认为是异常的。

    如果机器 A 和机器 B 建立了 TCP 连接,并且在进行了一些交换之后,机器 A 意识到连接中存在信息的不一致,它将发送一个包含 RST 标志位的数据包,请求机器 B 同意关闭此连接。这一次,连接的终止不是用 “四次挥手” 了。

  • fossi:我的初始暗号是1,你小子确认的话告诉我2就行了

  • stephen:我的初始暗号是99,你小子也给我确认下,到时候告诉我100就好了。对了,我给你确认值是25

    同样地,如果我将 SYN 数据包(用于请求建立连接)发送到已关闭的机器的端口,该机器会回复 RST 数据包,以告诉我所请求的端口未在监听。

五、数据互发阶段

hello完后,fossistephen开始巴拉巴拉了,正常情况下是这样通话的:

  • fossi:第一件事你听清楚了吗?

  • stephen:嗯,清楚了,你继续说

  • fossi:第二件事你听清楚了吧?

  • stephen:嗯,了解了,还有没有其他事了?有你就你继续说

  • 巴拉巴拉...

如果遇到下面这个情况可就麻烦了:

  • fossi:第一件事你听清楚了吗?

  • stephen:...

  • fossi:???听清楚了吗?

  • stephen:...

  • fossi:尼玛...

    回到主题,经过 “三次握手” 的过程,双向通信已经建立起来了,应用程序之间可以互传数据包了。在互传数据的过程中,发送的所有数据包上都会设置 ACK 标志,以确认收到了先前的数据包:

    当然了,不是每个数据包都要回复 ACK 的,比如客户端发了1,2,3,4,5五个数据包,如果服务端返回的 ACK 是5,说明前面四个数据包也都已成功接收到!

五、经典问题:为什么是三次握手?而不是两次或四次?

①首要原因:为了防止旧的重复连接初始化造成混乱。

    在复杂的网络环境下,数据包不一定如愿准时送到,可能会发出去很久还没收到回复,此时客户端会重试,万一老的报文在历经千辛万苦之后,突然又送到对方那里了呢?三次握手可以解决这个问题,如下图:

    那么防止这个问题的关键就是客户端判断服务端返回的 ACK 值是不是符合预期值,不是(可能是过期)则发 RST 报文中止本次连接。

    如果只有两次握手的话,就不能判断当前连接是否是历史连接。

②原因2:同步双方初始序列号

    上面我们说了序列号,这个东西太重要了,有了它,我们才能达到以下目的:

  • 接收方可以去除重复的数据;

  • 接收方可以根据数据包的序列号按序接收;

  • 可以标识发送出去的数据包中, 哪些是已经被对方收到的;

    可见,序列号在 TCP 连接中占据着非常重要的作用,所以当客户端发送携带「初始序列号」的 SYN 报文的时候,需要服务端回一个 ACK 应答报文,表示客户端的 SVN 报文已被服务端成功接收,那当服务端发送「初始序列号」给客户端的时候,依然也要得到客户端的应答回应,这样一来一回,才能确保双方的初始序列号能被可靠的同步。

③原因3:避免资源浪费

    其实这个就是原因①,如果只有两次握手,当客户端的 SYN 请求连接在网络中阻塞,客户端没有接收到 ACK 报文,就会重新发送 SYN ,服务端就会建立两个连接并保持,这里就会有无效连接保持着,浪费了资源。

至于为什么不是4次,原因是服务端返回的 SYN 和 ACK 是可以合并的,不需要四次。

因此,不使用「两次握手」和「四次握手」的原因:

  • 「两次握手」:无法防止历史连接的建立,会造成双方资源的浪费,也无法可靠的同步双方序列号;

  • 「四次握手」:三次握手就已经理论上最少可靠连接建立,所以不需要使用更多的通信次数。

六、总结

本文主要探讨了三次握手的细节,以下是本文重点:

  • 理解 TCP 头中的这些字段的含义:序列号、确认号、ACK、RST、SYN、FIN

  • 三次握手的过程 TCP 首部字段的变化

  • 三次握手的状态变化

  • 数据互发阶段的确认机制初步了解

  • 为什么是三次握手?而不是两次或四次?