从TCP的“三次握手”和“四次分手”讲起

说起TCP中最常见最重要的问题当然就是“三次握手”、“四次分手”了。在此之前,我们先来预热一下TCP的基本知识。

TCP报文段结构

TCP-Header

Source PortDestination Port:即源端口号和目的端口号,被用于多路复用/多路分解来自或送到上层应用的数据。
Sequence Number(32 bit):是包的序号,用来解决网络包乱序(reordering)问题。
Acknowledgment Number(32 bit):是确认号,用于确认收到,用来解决不丢包的问题。
Window(16 bit):也称接收窗口,该字段用于流量控制,指示接收方愿意接收的字节数量。以后会谈到。
Offset(4 bit):即首部长度字段,指示了以32比特的字为单位的TCP首部长度,由于TCP选项字段的原因,TCP首部的长度是可变的。
TCP Options:选项字段,用于发送方和接收方协商最大报文段长度(MSS)时,或在高速网络环境下用作窗口调节因子时使用。
TCP Flags(6 bit):即TCP的标志字段,就是包的类型,主要是用于操控TCP的状态机。


关于TCP报文头部的标志位

  • URG 紧急指针
  • ACK 确认序号
  • PUSH 接收方应当尽快将这个报文段交给应用层
  • RST 重置。重建链接
  • SYN 同步序号用来发起一个连接
  • FIN 完成发送

正确理解序号与确认号

首先要明白TCP将数据看成是一个无结构、有序的字节流。发送端的TCP会隐式地对数据流中的每一个字节编号。一个报文段的序号指的是该报文段首字节的字节流编号。
举个例子,假设主机A上的一个进程向通过一条TCP连接向主机B上的一个进程发送一个数据流。假定数据流由一个包含500000字节的文件组成,其MSS(TCP数据包每次能够传输的最大数据分段)为1000字节,数据流的首字节编号是0,该TCP将为该数据流构建500个报文段,给第一个报文段分配序号0,第二个报文段分配序号1000,第三个分配为2000......每一个序号被填入相应的TCP报文段首部的序号字段中。
注意的是,每一个报文段的大小不一定是MSS,而且下一个要发送的报文段大小还取决于接收方的窗口大小,这在之后的流量控制会谈到。

文件数据划分成TCP报文段

由此我们试想一下,当已知一个TCP连接中正发送一个包的序号为1500,我们可以知道,该端已经发送了1500字节的数据。所以,序列号又可以记忆为:当前端已经发送的数据位数,是否确认送达这还不一定。
确认号的增加是和传输的字节数相关的,除了数据占用字节数之外,每发送一次有效标志位SYN或FIN也算1位数据。

现在谈一下确认号。由于TCP是全双工通信的,同一条TCP连接的两端会相互接收和发送数据。当主机B收到来自于主机A的一个报文段,需要给主机A回个信息说明已成功收到,这个信息就是确认号。
主机B填充进报文段的确认号是主机B期望从主机A收到的下一个报文段的序号,或者也可以记忆成:当前端成功接收的数据位数。下面举两个例子,一个是正常情况下的数据传输,一个是出现差错的数据传输。
情况一,假设主机B收到了来自主机A的编号为0~535的所有字节(此报文段序号为0),同时打算发送一个报文段给主机A,其中的确认号字段填充为536。说明主机B等待主机A的数据流中字节编号为536及之后的所有字节,或者也可以理解为,主机B已经成功接收到了536位数据。
情况二,假设主机A向主机B发送了字节编号为0-535,536-899,900-1000的三个报文段(报文段序号为0,536,900)。由于某种原因(报文段丢失),主机B没有收到字节编号536-899的报文段。那么问题来了,由于第三个报文段失序到达,该怎么处理呢?如果回复的确认号为1001,会使得主机A认为三个报文段均安全到达,显然这么做事不合理的。然而对于这种TCP连接中收到的失序报文段,TCPRFC并未明确规定任何规则,故需人为实现。现有两个选择:
1、立即丢弃失序报文段;
2、保留报文段,并采取合理的方法等待缺失的报文段补齐。
显然,后者对网络带宽而言更为有效,具体采取什么样的方法等待补齐,这个问题在后面的TCP重传机制上会有深刻讨论。

为了更好理解序号和确认号,可以用WireShark创建一个TCP流的图形摘要。下面这个摘要图是从网上copy来的,不过不影响,我们只是用来解读的。

TCP流摘要图

首先,每行代表一个单独的TCP报文段,左边列显示时间,中间列显示包的方向、TCP端口、段长度和设置的标志位,右边列以10进制的方式显示相关序号/确认号,这里的序号/确认号用的是相对序号/确认号。
这里我们做个约定:箭头左侧代表客户端(端口号54841),箭头右侧代表服务器(端口号80)。
报文段1:客户端发送一个特殊的报文段给服务器端,该报文段叫SYN报文段(报文段首部仅SYN标志位被置1),不含任何应用层数据。另外,客户端会随机选择一个初始序号,由于是初始序号,其相对序号为0,也就是 Seq=0。
报文段2:包含TCP SYN报文段的IP数据报到达服务器主机,服务器会从该数据报中提取SYN报文段,并为该TCP连接分配TCP缓存和变量,同时向客户端发送允许连接的报文段,这个报文段称为SYNACK报文段,也不包含应用层数据,但其首部包含3个重要的信息。
首先SYN标志位被置1,其次,确认号Ack被置为1,最后服务器选择自己的初始序号,由于是初始序号,其相对序号为0,也就是 Seq=0。
需要注意的是,尽管客户端没有发送任何有效数据,确认号还是被加1,这是因为接收的包中包含SYN或FIN标志位(并不会对有效数据的计数产生影响,因为含有SYN或FIN标志位的包并不携带有效数据)。
报文段3:客户端在收到SYNACK报文段之后,客户端也要给该TCP连接分配缓存和变量。客户端向服务器发送另外一个报文段,表示对服务器的允许连接进行了确认,由于已经建立了连接,所以SYN置0(此时可以携带有效数据了)。序号Seq为1,表示已经发送了1位数据(即报文段1的SYN),确认号Ack为1,表示已经接收到了1位数据(即报文段2的SYN)。
报文段4:这是流中第一个携带有效数据的包(比如说是客户端发送的HTTP请求),序号依然为1,因为到上个报文段为止,还没有发送任何数据,确认号也保持1不变,因为客户端没有从服务端接收到任何数据。这里有效数据的长度为725字节。
报文段5:服务器端收到了来自客户端发送的请求数据(共725字节)后,发送报文段确认收到,确认号的值增加了725(725是包4中有效数据长度),变为726,简单来说,服务端以此来告知客户端端,目前为止,我总共收到了726字节的数据,服务端的序列号保持为1不变(因为到报文段5为止,服务器还未发送过数据)。
报文段6:这个报文段标志着服务端对客户端请求响应的开始,序列号依然为1,因为服务端在该报文段之前返回的报文段中都不带有有效数据,该报文段带有1448字节的有效数据。
报文段7:此报文段用于对报文段6中1448字节数据的确认。由于上个数据报文段(报文段4)的发送,TCP客户端的序列号增长至726,从服务端接收了1448字节的数据,客户端的确认号由1增长至1449。

接下来就是重复类似的过程。

小Tip
从上面的图可以看到,除了第一次握手的报文段SYN之外,其他所有报文段都必须有Ack。为什么呢?
TCP作为一个可靠的传输协议,其可靠性是依赖于收到对方的消息并回复确认给对方,所以任何方发送的TCP报文段都要捎带着ACK状态位。ACK状态位要有Acknowledge Number配合才行。

下面再来补充一下有关初始化序号的知识点(缩写为ISN:Inital Sequence Number)

关于ISN

为什么ISN要动态随机?
事实上,一条TCP连接的双方均可以随机地选择初始序号,这样可以减少将那些仍在网络中存在的来自两台主机之间先前已经终止的连接报文段,误认为是后来这两台主机之间新连接所产生的有效报文段的可能性(碰巧与旧连接使用了相同端口号)。
这句话听起来有点绕口,举个例子,比如:如果连接建好后始终用1来做ISN,如果client发了30个segment过去,但是网络断了,于是 client重连,又用了1做ISN,但是之前连接的那些包到了,于是就被当成了新连接的包,此时,client的Sequence Number 可能是3,而Server端认为client端的这个号是30了。全乱了。
再有,ISN动态随机使得每个tcp session的字节序列号没有重叠,如果出现tcp五元组冲突这种绩效概率情况的发生,一个session的数据也不会被误认为是另一个session的。

ISN会和一个假的时钟绑在一起,这个时钟会在每4微秒对ISN做加一操作,直到超过2^32,又从0开始。这样,一个ISN的周期大约是4.55个小时。因为,我们假设我们的TCP Segment在网络上的存活时间不会超过Maximum Segment Lifetime(缩写为MSL),所以,只要MSL的值小于4.55小时,那么,我们就不会重用到ISN。


TCP的连接管理

所谓TCP的连接管理也就是“三次握手”、“四次分手”的过程。在这个部分中我们会仔细的分析如何建立和拆除一条TCP连接。


tcp_open_close

建立连接

其实在前面的序号和确认号分析中我们已经走了一遍建立连接的三次握手流程,这里做个总结:

  • 第一步:客户端发送一个特殊的报文段给服务器端,该报文段叫SYN报文段(报文段首部仅SYN标志位被置1),不含任何应用层数据。另外,客户端会随机选择一个初始序号x,并将此编号放置于该起始的TCP SYN报文段的序号段中,然后被封装在一个IP数据报中,并发送给服务器。
  • 第二步:包含TCP SYN报文段的IP数据报到达服务器主机,服务器会从该数据报中提取SYN报文段,并为该TCP连接分配TCP缓存和变量,同时向客户端发送允许连接的报文段,这个报文段称为SYNACK报文段,也不包含应用层数据,但其首部包含3个重要的信息。首先SYN标志位被置1,其次,确认号Ack被置为x+1,最后服务器选择自己的初始序号y,并将其放置到TCP报文段首部的序号字段中。
    这个允许连接的报文段实际上表明了:“我收到了你发起建立连接的SYN分组,该分组带有初始序号x。我同意建立该连接,我自己的序号是y”。
  • 第三步:客户端在收到SYNACK报文段之后,客户端也要给该TCP连接分配缓存和变量。客户端向服务器发送另外一个报文段,表示对服务器的允许连接进行了确认,由于已经建立了连接,所以SYN置0(此时可以携带有效数据了)。三次握手的第三个阶段可以在报文段负载中携带客户到服务器的数据。

关于TCP3次握手的几个讨论
1、为什么需要初始化序号?
首先要明白,对于建链接的3次握手,实际上在握什么?握的是数据原点的序号。通信的双方要互相通知对方自己的初始化的序号(上面的x和y)。这个号要作为以后的数据通信的序号,以保证应用层接收到的数据不会因为网络上的传输的问题而乱序(TCP会用这个序号来拼接数据)。

2、三次握手的第一次为什么不可以携带数据?
因为握手还没成功。
那难道不可以将数据缓存下来等握手成功再提交吗?
不行,这样容易受到SYN FLOOD攻击。如果第一次可携带数据,攻击者会伪造成千上万的握手报文,每个报文携带大量数据,那么接收端要开辟大量内存来缓存这些巨大的数据,内存容易耗尽,从而拒绝服务。

3、第三次为什么可以携带数据?
能够发出第三次握手报文段的主机肯定接收到了第二次的握手报文段,因为伪造IP的主机是不会收到第二次报文的(为啥呢?)。所以能够发出第三次握手报文的主机应该是合法用户。
再次,经过三次握手,通信双方已确认初始序列,也为对方开辟了临时内存与变量。客户端发出第三次报文段的瞬间进入“ESTABLISHED”状态,单方面宣告连接建立,可发送数据,尽管服务器侧状态还没“建立”,但接收到第三次握手的瞬间就会切换到“ESTABLISHED”,里面携带的数据就会按正常流程走好。
4、为什么是三次握手而不是两次或者四次?
试想一下两次握手的过程:
Client:SYN+Client ISN
Server:SYN+Server ISN+Server Ack
这里有一个问题,Client与Server就Client的初始序列号达成了一致,但是Server无法知道Client是否收到自己的初始序号和确认号,如果丢失,Client与Server就Server的初始序列号无法达成一致。

于是TCP设计者将标志位SYN(FIN)设计成占用一个字节的编号,既然是一个字节的数据,按照TCP对有数据的TCP报文段必须确认的原则,这里Client必须给Server一个确认,以确认Client已经收到Server的同步信号SYN。

这里还有一个问题,就是如果Client发给Server的确认中途丢失了怎么办,Client会重传这个ACK吗?不会的,TCP不会为不带数据的ACK执行重传!解决的办法是,Server会重传自己的SYN同步信号,直至收到Client的ACK信号。

再想想四次握手的过程:
Client:SYN+Client ISN
Server:SYN+Server ISN+Server ACK
Client:SYN+Client ISN+Client ACK
Server:SYN+Server ISN+Server ACK
......
然后没完没了了,其实四次和五次六次七次八次是一个道理。
只有三次刚刚好保证数据的可靠传输和传输的效率。
下面是一个绘声绘色的段子供大家理解(来自某乎):


某乎截图

断开连接

对建立连接有了理解之后,断开连接的道理也就很好懂了。举个例子,假设某客户端打算关闭连接:

  • 第一步:客户应用进程发出一个关闭连接命令,这会引起客户TCP想服务器进程发送一个特殊的TCP报文段,称为FIN报文段(首部只有一个标志位FIN置1)。
  • 第二步:服务器收到FIN报文段后就向客户端会送一个确认报文段;
  • 第三步:服务器发送自己的终止报文段,FIN置为1;
  • 第四步:最后,客户端对这个服务器的终止报文段进行确认,此时,两台主机上用于该连接的所有资源都被释放了。

我们再来分析一下这四次的过程:
Client:FIN , Server:ACK
Client属于主动关闭方,当Client收到来自Server的ACK之后,进入半关闭状态,这时候,Client不能再发送数据了。这时Server还可以单向发送数据,Server数据发送完了,也做关闭动作。

Server:FIN , Client:ACK
Client收到Server发送的FIN后,马上回复确认关闭ACK,等待2MSL时间后,连接正式关闭,客户端所有资源(包括端口号)将被释放。(为什么需要等待2MSL?下面接着分析)

有个小问题:为什么第二次和第三次分手不合在一起呢?也就是Server的ACK和FIN不一起发送?
其实,当客户主动请求关闭连接,说明客户端已经不再请求数据了,得到服务器端的确认后,服务器并没有马上发起自己的关闭,是因为服务器端还有一些剩余的数据未发完,直到数据发送完毕后再发起关闭动作。


客户端经历的TCP状态序列

其实,网络上的传输是没有连接的,包括TCP也是一样的。而TCP所谓的“连接”,其实只不过是在通讯的双方维护一个“连接状态”,让它看上去好像有连接一样。所以,TCP的状态变换是非常重要的。
在一个TCP连接的生命周期内,运行在每台主机中的TCP协议在各种TCP状态之间变迁。TCP的状态是由TCP报文段的标志位控制的。下图说明了客户TCP会经历的一系列典型TCP状态。


客户TCP经历的典型的TCP状态

现在我们来走一下这个状态流程:

  • CLOSED-->ESTABLISHED
    客户TCP开始处于CLOSED(关闭状态),客户端的应用进程发起一个新的TCP连接,这引起客户中的TCP向服务器中的TCP发送一个SYN报文段,在发送SYN报文段过后,客户TCP进入SYN_SENT状态。此时客户TCP等待来自服务器TCP的对客户所发送报文段进行确认且SYN被置1的一个报文段。收到这个报文段之后,客户TCP单方面进入ESTABLISHED(已建立)状态。当处在ESTABLISHED状态时,TCP客户就能发送和接收包含有效载荷数据(即应用层产生的数据)的TCP报文段了。
  • ESTABLISHED-->CLOSED
    假设客户应用进程决定关闭该连接(注意到服务器也能选择关闭该连接)。这引起客户TCP发送一个FIN特殊报文段,并进入FIN_WAIT_1状态,等待一个来自服务器带有确认的TCP报文段。当收到该报文段时,客户TCP进入FIN_WAIT_2状态,此时客户TCP处于半关闭状态,不能再发送数据了。处于FIN_WAIT_2状态的客户TCP等待来自服务器的FIN特殊报文段,当收到该报文段后,客户TCP对服务器进行确认,并进入TIME_WAIT状态。经过2MSL等待之后,连接正式关闭,客户端释放所有资源。


    关闭一条TCP连接

现在来解释一下为什么主动关闭方最后为什么要等待2MSL之后才关闭连接?
主要有两点:
(1)可靠的实现TCP全双工连接的终止;
(2)语序来到重复的分节在网络中消逝;
先说第一点,当客户端发送ACK确认报文段给服务器之后,客户端无法得知服务器是否收到ACK,所以我们定一个规则:在客户端发送ACK确认报文段之后的2MSL的期间内未收到服务器发送的FIN报文段,则认为服务器已成功接收到了该ACK确认报文段,所以TCP连接可完全关闭。
为什么是2MSL呢?这只是一个保守的估计时间。所谓MSL是Maximum Segment Life,这是TCP 对TCP Segment 生存时间的限制,任何报文段在丢弃之前在网络中内的最大生存时间。假如ACK没有到达服务端,服务端会为FIN这个消息超时重传 timeout retransmit ,那如果客户端等待时间足够,又收到FIN消息,说明ACK没有到达服务端,于是再发送ACK,直到在足够的时间内没有收到FIN,说明ACK成功到达。这个等待时间至少是:服务端的timeout + FIN的传输时间,为了保证可靠,采用更加保守的等待时间2MSL。

再说第二点,如果Client直接CLOSED,然后又再向Server发起一个新连接,我们不能保证这个新连接与刚关闭的连接的端口号是不同的。也就是说有可能新连接和老连接的端口号是相同的。一般来说不会发生什么问题,但是还是有特殊情况出现:假设新连接和已经关闭的老连接端口号是一样的,如果前一次连接的某些数据仍然滞留在网络中,这些延迟数据在建立新连接之后才到达Server,由于新连接和老连接的端口号是一样的,又因为TCP协议判断不同连接的依据是socket pair,于是,TCP协议就认为那个延迟的数据是属于新连接的,这样就和真正的新连接的数据包发生混淆了。所以TCP连接还要在TIME_WAIT状态等待2倍MSL,这样可以保证本次连接的所有数据都从网络中消失。

服务器经历的TCP状态序列

服务器TCP经历的典型TCP状态

服务器的TCP状态比较好解释,大家可以对着图自行理解。


至此,关于TCP的一点点点点知识算是讲完了,不过后续的还有流量控制,拥塞控制等等,敬请关注!
推荐公众号:车小胖谈网络

最后编辑于
©著作权归作者所有,转载或内容合作请联系作者
  • 序言:七十年代末,一起剥皮案震惊了整个滨河市,随后出现的几起案子,更是在滨河造成了极大的恐慌,老刑警刘岩,带你破解...
    沈念sama阅读 157,198评论 4 359
  • 序言:滨河连续发生了三起死亡事件,死亡现场离奇诡异,居然都是意外死亡,警方通过查阅死者的电脑和手机,发现死者居然都...
    沈念sama阅读 66,663评论 1 290
  • 文/潘晓璐 我一进店门,熙熙楼的掌柜王于贵愁眉苦脸地迎上来,“玉大人,你说我怎么就摊上这事。” “怎么了?”我有些...
    开封第一讲书人阅读 106,985评论 0 237
  • 文/不坏的土叔 我叫张陵,是天一观的道长。 经常有香客问我,道长,这世上最难降的妖魔是什么? 我笑而不...
    开封第一讲书人阅读 43,673评论 0 202
  • 正文 为了忘掉前任,我火速办了婚礼,结果婚礼上,老公的妹妹穿的比我还像新娘。我一直安慰自己,他们只是感情好,可当我...
    茶点故事阅读 51,994评论 3 285
  • 文/花漫 我一把揭开白布。 她就那样静静地躺着,像睡着了一般。 火红的嫁衣衬着肌肤如雪。 梳的纹丝不乱的头发上,一...
    开封第一讲书人阅读 40,399评论 1 211
  • 那天,我揣着相机与录音,去河边找鬼。 笑死,一个胖子当着我的面吹牛,可吹牛的内容都是我干的。 我是一名探鬼主播,决...
    沈念sama阅读 31,717评论 2 310
  • 文/苍兰香墨 我猛地睁开眼,长吁一口气:“原来是场噩梦啊……” “哼!你这毒妇竟也来了?” 一声冷哼从身侧响起,我...
    开封第一讲书人阅读 30,407评论 0 194
  • 序言:老挝万荣一对情侣失踪,失踪者是张志新(化名)和其女友刘颖,没想到半个月后,有当地人在树林里发现了一具尸体,经...
    沈念sama阅读 34,112评论 1 239
  • 正文 独居荒郊野岭守林人离奇死亡,尸身上长有42处带血的脓包…… 初始之章·张勋 以下内容为张勋视角 年9月15日...
    茶点故事阅读 30,371评论 2 241
  • 正文 我和宋清朗相恋三年,在试婚纱的时候发现自己被绿了。 大学时的朋友给我发了我未婚夫和他白月光在一起吃饭的照片。...
    茶点故事阅读 31,891评论 1 256
  • 序言:一个原本活蹦乱跳的男人离奇死亡,死状恐怖,灵堂内的尸体忽然破棺而出,到底是诈尸还是另有隐情,我是刑警宁泽,带...
    沈念sama阅读 28,255评论 2 250
  • 正文 年R本政府宣布,位于F岛的核电站,受9级特大地震影响,放射性物质发生泄漏。R本人自食恶果不足惜,却给世界环境...
    茶点故事阅读 32,881评论 3 233
  • 文/蒙蒙 一、第九天 我趴在偏房一处隐蔽的房顶上张望。 院中可真热闹,春花似锦、人声如沸。这庄子的主人今日做“春日...
    开封第一讲书人阅读 26,010评论 0 8
  • 文/苍兰香墨 我抬头看了看天上的太阳。三九已至,却和暖如春,着一层夹袄步出监牢的瞬间,已是汗流浃背。 一阵脚步声响...
    开封第一讲书人阅读 26,764评论 0 192
  • 我被黑心中介骗来泰国打工, 没想到刚下飞机就差点儿被人妖公主榨干…… 1. 我叫王不留,地道东北人。 一个月前我还...
    沈念sama阅读 35,412评论 2 269
  • 正文 我出身青楼,却偏偏与公主长得像,于是被迫代替她去往敌国和亲。 传闻我的和亲对象是个残疾皇子,可洞房花烛夜当晚...
    茶点故事阅读 35,299评论 2 260

推荐阅读更多精彩内容