一、前言
今天刚看完《计算机网络——自顶向下方法》这本书的运输层这一章。直到今天我才知道,TCP
协议居然有这么复杂(之前上课都没怎么认真听),这一章节总共七十多页,主要介绍UDP
与TCP
,但UDP
的内容却只占不超过5
页,大部分的篇幅都是在讲解TCP
的机制。但是就算这样,我还是感觉这本书对TCP
的讲解不够详细,忽略了很多内容和细节,导致我无法将这本书对TCP
的讲解串通在一起,感觉前后矛盾,同时也有很多地方无法理解。这一章读下来,我对TCP
有了更深入的了解,但是同时也对它有了更多的疑问。由此看来,想要真正弄懂TCP
,不能仅仅依靠这种入门书籍,应该选择一些专门讲解这一块内容的书籍进行研究。
接下来我准备将这几天看的TCP
的内容进行一次总结,以博客的形式将这些内容记录下来,算是加深印象吧。这篇博客就先来说一说TCP
的三次握手和四次挥手,也就是TCP
连接的建立与断开,算是TCP
中相对比较简单的部分。
二、解析
2.1 TCP的报文格式
想要了解TCP
的三次握手与四次挥手,首先得了解一下TCP
的报文格式,因为这两个过程需要依赖TCP
报文中的一些字段。下面我们通过一幅图来讲解TC
P的报文格式:
我只介绍TCP
报文中,与三次握手和四次挥手有关的部分:
- 源端口号:本次
TCP
连接中,发起连接的主机使用的端口号; - 目的端口号:本次
TCP
连接主,接受连接的主机使用的端口号; - 序号:通过TCP传输的每一个数据段,都有一个序号,作用是为了确认此数据段的顺序。网络中允许传输的数据长度是有限制的,所以当我们要通过
TCP
传输一个较大的数据时,TCP
会将数据切割成很多小的数据段进行传输。而将这些小的数据段发送到目的主机时(发送方会同时发送多个数据),并不能保证它们是按顺序到达目的地,所以对于每一个数据段,都要有一个序号,来标识它们是属于总数据的哪一部分,以保证在目的主机中能将他们重新拼接。 - 确认序号:接收方若接收到一个数据段,会发送一个确认报文给发送方,告诉发送方已经接收到这个数据段,而确认序号的作用就是告诉发送方接收到了哪条数据段。若接收方接收到了序号为
n
的报文段,则确认序号将是n+1
,表示它已经接收了n
,下一条想要接收n+1
; - 首部长度:
TCP
报文的首部+数据的字节数; - ACK:只有
1 bit
的标志位,若为1
,表示这个数据段中的确认序号是有效的,即这个数据报是对之前接收到的某个报文的确认(一个TCP
报文可以同时作为确认报文和传递数据报文)。 - RST:只有
1 bit
的标志位,若客户端向服务器的一个端口请求建立TCP
连接,但是服务器的那个端口并不允许建立连接(比如没开启此端口),则服务器会回送一个TCP
报文,将RST
位置为1,告诉客户端不要再向这个端口发起连接; - SYN:只有
1 bit
的标志位,若为1
,表示这是一条建立连接的TCP
报文段; - FIN:只有
1 bit
的标志位,若为1
,表示这是一条断开连接的TCP
报文段;
对于TCP
报文格式,就先介绍这么多,其余的部分虽然也很重要,但是并没有作用于TCP
连接的建立与断开,所以就不在这里叙述了。
2.2 TCP三次握手的过程
TCP
建立连接的过程中需要发送三次报文,所以TCP
建立连接也被称为三次握手,接下来我就来讲讲这三次握手的过程,假设客户端向服务器发起TCP
连接:
第一步:客户端的
TCP
程序首先向服务器的TCP
程序发送一个TCP
报文。这个报文不包含数据,且它的SYN
标志位被置为1
,表示这是一条建立连接的TCP
报文段,因此这个报文段也被称为SYN报文段。客户端的TCP
程序随机选择一个序号作为客户端报文的初始序号(假设序号为client_isn
),放入这个报文段的序号部分。这个报文段由运输层传递到网络层后,被封装在一个IP
数据报中发往服务器;第二步:包含
SYN
报文段的IP
数据报被服务器接收,服务器的网络层将SYN
数据报抽取出来,交给运输层,同时服务器为该TCP
连接分配资源(包括发送缓存、接收缓存和变量等),并向客户发送允许连接的TCP
报文段。这条允许连接的报文段不包含数据,SYN
标志位也被置为1
,同时它的ACK
标志位也被置为1,表示它是SYN
报文段的确认报文,所以这条允许连接的报文段也被称为SYNACK报文段。服务器随机选择一个序号,作为服务器报文段的初始序号(假设称为server_isn
),并将其放入SYNACK
报文段的序号部分,同时确认号字段被设置为client_isn + 1
(SYN
报文段的序号+1)。这个报文段可以解释为服务器向客户端说:“我收到了你的连接请求,我允许你连接,我的初始序号是server_isn
”。第三步:当客户端接收到
SYNACK
报文段后,它也将为TCP
连接分配资源(缓存和变量),同时生成一条SYNACK
报文段的确认报文,并发送给服务器。由于经过上面两个步骤,已经算是建立了连接,所以这次的SYN
标志位将被置为0
,而不是1
(ACK
标志位是1
)。同时,这条报文段的序号被设置为client_isn + 1
(第一条客户报文的序号是client_isn
,而这是它的下一条,所以+1
),而确认序号被设置为server_isn + 1
(第一条服务器报文的序号是server_isn
,客户端成功接收,所以期望服务器下一次发送server_isn + 1
)。和上面两条报文不同,第三条报文可以携带数据,比如HTTP
的请求就是在TCP
的第三次握手报文中发送到服务器的。
经过上面这三个步骤,TCP
连接就算正式建立完毕,客户端和服务器可以相互发送数据了。下面是这个过程的图片形式:
2.3 为什么是三次握手而不是两次
首先我们要明确,两次握手是必要的。第一次握手,客户端将SYN
报文发送到服务器,服务器接收到报文后,即可确认客户端到服务器是可达的;而服务器向客户端发送响应的SYNACK
报文,客户端接收到后,即可确认服务器到客户端也是可达的。至此,连接已经算是建立,那为什么还要有第三次握手呢?
客户端和服务器的握手过程,不仅仅是确认互相可达的过程,更重要的是一个同步的过程,SYN
就是同步(Synchronize)的缩写。对于TCP
报文段来说,序号是一个至关重要的部分,它保证了TCP
传输数据的完整性。而我们上面也说过,TCP
报文的初始序号不是从0
开始的,而是一个随机的序号,而所谓的同步,就是TCP
客户端和服务器互相同步初始序号的过程。第一次握手,客户端发送SYN
报文,将自己的初始序号发送到了服务器,服务器接收到后,向客户端发送SYNACK
报文段,告诉客户端已经收到了它的初始序号,同时在这个报文段中带上了自己的初始序号。这个时候,第三次握手的作用就出来了:第三次握手实际上就是客户端在告诉服务器,自己已经收到了它的初始序号,完成了同步,可以开始相互传输数据了。若没有第三次握手,服务器将无法保证客户端接收到了自己的SYNACK
报文段,若此时SYNACK
报文段丢失,客户端不知道服务器的初始序号,将无法处理之后到达客户端的数据。
在很多书籍和网上的博客中还流传另外一种说法。若仅仅是两次握手,将产生以下问题:客户端向服务器发送SYN
报文段请求建立连接,但是没有在指定时间内收到SYNACK
报文段,所以客户端认为SYN
报文段在网络中丢失,则再次发送SYN
报文段,并成功接收到了SYNACK
报文段,但是客户端在很短的时间内就断开了TCP
连接。然而,最初的SYN
报文并没有丢失,只是传输时延太长,过了许久才到达。等它到达服务器时,其实客户端已经与服务器建立过TCP
连接,并且已经断开了。此时服务器接收到这条SYN
报文段,以为客户端又想建立一条新的连接,于是向客户端回送ACK
报文,并为连接分配了资源。由于没有第三次握手,服务器将不知道这其实是上一次连接的报文,于是将它创建一个新的TCP
连接并维持,直至因为太久没有接收到数据而释放。这种情况非常浪费资源,所以为了防止这种情况的发生,才需要客户端的再一次确认。
实际上,上面所述的第一点才是TCP
三次握手的原因,第二个只能算是顺带的好处吧,从建立连接的报文被称为SYN
(同步)就可以看出这点。
2.4 TCP四次挥手的过程
说完了三次握手,下面来说说四次挥手的过程。TCP
在断开连接时,客户端与服务器之间要交换四次报文,所以,TCP
的断开连接也叫四次挥手。
- 第一步:客户端进程发出断开连接指令,这将导致客户端的
TCP
程序创建一个特殊的TCP
报文段,发送到服务器。这个报文段的FIN
字段被置为1,表示这是一条断开连接的报文; - 第二步:服务器接收到客户端发来的断开连接报文,向客户端回送这个报文的确认报文(
ACK
字段为1
),告诉服务器已经接收到FIN
报文,并允许断开连接; - 第三步:服务器发送完确认报文后,服务器的
TCP
程序创建一条自己的断开连接报文,此报文的FIN
字段被置为1
,然后发往客户端; - 第四步:客户端接收到服务器发来的
FIN
报文段,则产生一条确认报文(ACK
为1
),发送给服务器,告知服务器已经接收到了它的断开报文。服务器接收到这条ACK
报文段后,释放TCP
连接相关的资源(缓存和变量),而客户端等待一段时间后(半分钟、一分钟或两分钟),也释放处于客户端的缓存和变量;
以上就是四次挥手的过程,相对建立连接来说要简单一些。以上是以客户端请求断开连接来举例,但其实也可以由服务器断开连接。
2.5 客户端为什么要等待一段时间再释放资源
看完上面四次挥手的过程,可能有的人会有疑问,四次挥手之后,连接不是已经断开了吗,为什么客户端还要等待一段时间再释放资源呢?原因有两个:
- 原因一:客户端接收到服务器发送的
FIN
报文后(第三次挥手),会回送一条确认报文(第四次挥手),但是,客户端并不知道这条确认报文是否可以顺利到达服务器。若这条确认报文在传送到服务器的过程中损坏、丢失或超时,将引起服务器重新发送FIN
报文,客户端接收到后,将需要再次发送一条确认报文,直到服务器正确接收。但是,客户端发送确认报文后,立刻释放资源,将导致无法处理重传的FIN
报文,所以客户端需要等待一段时间,直到确认没有出现上述情况出现再释放资源。 - 原因二:
TCP
四次挥手完成后,理论上已经断开了连接,但是这不代表之前通过这条连接发送的所有数据都处理完毕了,有些可能还在网络中传输。若在四次挥手后,立即释放客户端的资源,然后客户端立即以同一个源端口,向服务器的同一个目的端口再次建立一个TCP
连接,这个连接和上一个的 源端口+源IP
+目的端口+目的IP
都一模一样,此时将会产生问题。若上一次连接遗留在网络中的报文此时到达,将会被当做新连接传输的数据处理,于是可能会产生一些不可预估的错误。所以,客户端在断开连接后,需要等待一段时间,直到网络中遗留的数据都死掉,才释放资源,而在资源没有被释放前,是不允许建立一个 源端口+源IP
+目的端口+目的IP
都一模一样的TCP
连接的(因为TCP
套接字由这四部分标识)。
2.6 断开连接为什么需要四次挥手
有的人可能也会想,断开连接为什么是四次挥手,不能是两次呢?其中一方请求断开连接,另一方确认即可,为什么这个过程需要两边各发起一次?原因就是:TCP连接是全双工的。什么是全双工,即A
与B
建立连接,则A
可以向B
发送数据,而B
也可以向A
发送数据。
我们知道断开连接的请求什么时候发起?当然就是在不再有数据需要发送时。我们依旧以客户端向服务器断开连接为例。假设客户端和服务器建立了一个TCP
连接,在客户端需要向服务器发送的数据都发送完后,客户端就可以向服务器发送一个FIN
报文段,请求断开连接;服务器接收到后,将会回送一个ACK
报文,告诉客户端,自己已经收到了它断开连接的请求。若只有两次挥手,这个时候连接就算是断开了。但是这样真的合理吗?答案当然是否定的。
客户端发送完数据后,告诉服务器,我没有数据了,可以和你断开,但是不代表服务器没有数据需要发送到客户端了呀。TCP
是一个全双工的连接,代表服务器也有可能有数据需要发送到客户端。所以,只有当两端的数据都发送完毕,连接才能安全的断开。因此,服务器接收到了客户端的FIN
报文段,他会等到自己所有的数据发送完,然后也向客户端发送一个FIN
报文,告诉客户端我也没数据了,这时候连接才能真正断开,两端各自释放资源。
三、总结
以上对TCP
的三次握手和四次挥手做了一个比较全面的讲解,相信看完之后可以让人对TCP
连接的建立和断开有更深入的理解。但是,这一部分内容实际上只是TCP
中比较简单的一块,尽管如此,还是用了这么多篇幅去讲解,可想而知TCP
的复杂性。我后面还将写几篇关于TCP其他方面的内容,希望在搜集资料和编写的过程中,能够加深我对它的理解。
四、参考
- 《计算机网络——自顶向下方法(原书第七版)》
- 多篇博客