网络原理 - 5(TCP - 2 - 三次握手与四次挥手)
目录
3. 连接管理
建立连接 - 三次挥手
三次握手的意义
断开连接 - 四次挥手
握手和挥手的相似和不同之处
连接管理过程中涉及到的 TCP 状态转换
完!
3. 连接管理
连接管理分为建立连接 和 断开连接~(important 重点!)
建立连接 - 三次挥手
TCP 是有连接的,我们在网络编程中客户端执行了一行代码:socket = new Socket(serverIp, serverPort); 这个操作就是建立连接!!!
上述我们代码中,只是调用了 socket 的 API,真正的建立连接的过程,是在操作系统的内核中完成的~
内核中是如何完成上述的“建立连接”的过程呢??? ==》 “三次握手”,此处我们建立的连接,是一种“虚拟的,抽象的”连接,目的是让通信双方都能保存对方的相关信息~
syn 其实是 synchronize 的缩写,有没有很熟悉,就是我们之前上锁的关键字~~~ 不过这里我们可以更形象的翻译为 同步~~~
在建立连接的过程中,客户端是主动的一方,第一次交互,一定是客户端主动发起的,所谓 syn,是一个特殊的 TCP 数据报(1. 没有载荷,不会携带应用层数据 2. 六个标志位中的第五位 SYN 为 1),这个 syn 就会表达一层语义:我想和你建立连接(虚拟抽象的连接)
虽然 syn 没有携带应用层载荷,但是也会有 IP 报头 / 以太网数据帧帧头,更会有 TCP 报头,TCP 报头中,就会包含了客户端自己的端口,IP 报头中,就会包含了客户端自己的 IP(这个过程,也是客户端在告诉服务器,who am i)
客户端发出 syn 同步报文段之后,会有两种可能性:
1. 服务器同意了,服务器表示,好呀好呀,我也想和你建立连接~~
2. 服务器没同意。这种情况一般比较少见,就是服务器现在处在负载非常高的情况下,完全没办法再进行响应了,此时的客户端太多了~~~也就没有下文了(只要服务器有空闲,就一定会想和客户端建立连接,毕竟服务器本身就是用来提供服务的~~~)
服务器接收到客户端的 syn 同步报文段之后,就会返回一个 ACK(确认应答),语义就是:收到!!!
接下来,服务器返回 ack 之后,还会再返回一个 syn,这个 syn 的意思就是,我接收你的连接(我也愿意和你建立连接,刚刚返回 ack 只是表明,我服务器收到了你的请求连接,现在返回 syn 是表明了,我也愿意和你建立连接~~~)
客户端在收到服务器发过来的 syn 之后,也会再给服务器发送一个 ack 报文,表示,好滴,我知道你愿意和我建立连接啦~~~
到此,客户端和服务器在内核中建立连接的过程就完成啦~~~
下面是一些细节的补充:
syn 我们形象的翻译为“同步”,实际上是不同的含义(同一个术语,在不同的语境下,有不同的意思~~)
多线程加锁的 synchronized 同步,是要协调多个线程之间的执行顺序。
TCP 这里的同步,则是建立连接状态,客户端服务器需要相互配合来完成一系列的工作业务了~~~也就是说,双方一旦建立连接之后,双方就得力往一处使,也可以理解为一种同步。
所谓的建立连接的过程,本质上就是通信双方各自给对方发送一个 syn,各自再给对方回应一个 ack,这里客户端的信息告知服务器,在第一次发送 syn(第一次握手)的时候就完成了,但最终这个连接要建立,还确认出后续还要进行通信,是需要所有流程都走完的。
虽然第一次握手,客户端就把自己的信息告诉服务器了,但是服务器具体是否要确定存储这个信息,还需要再观望官网~~~等所有的握手环节结束之后,服务器才会最终保存客户端的相关信息。
这里的过程叫做“握手” ==》 handshake 也是一个形象的比喻,握手是打招呼,syn 这样的数据报,不携带载荷,没有应用层数据,也就不代表任何应用程序的业务逻辑,syn 的作用,仅仅只是“打招呼”,所以我们在这里把这个操作形象的称为“握手“。 == 》 打完招呼之后,后面我们双方就可以商讨具体的细节了(商讨细节,就是业务逻辑,应用程序 / 应用层要完成的业务逻辑了~~~)
还有一个问题是,我们不是称为”三次握手“吗???握手,理解了,就是打招呼的意思,但上面明明是进行了 4 次交互过程呀~~~
其实其实,中间的两次操作,可以进行合并。在六个关键位中,SYN 和 ACK 是在不同位置上的,syn 是第五位,ack 是第二位,当发送 ack 的时候,就六个关键位的第二位为 1,当发送 syn 的时候,就六个关键位的第五位为 1 即可~~
完全可以做到,一个数据报,第五位和第二位都是 1,这样,这个数据报不就同时起到了两个作用吗,既能应答上个请求,也能发起 syn。(我们前面讲过,网络传输过程中要涉及到多次封装和分用。两个数据报就要封装分用两次,合并成一个数据报,就只需要封装分用一次,可以减少一次封装分用的过程,这样整体的效率就提升了,也会降低我们的成本~~)
其他资料上也会有”三次握手“的更为详细的描述,如下:
这种描述中,TCP 对应的状态转换(CLOSE LISTEN ESTABLISHED)对应的 socket 的 API等等,都更为详细的进行了介绍,我们这里就不过多详细了~~
三次握手的意义
1. 关注中间过程:三次握手,可以先针对通信路径,进行一个“投石问路”的操作,初步的确认一下通信链路是否畅通,这也是可靠性的前提条件~~~如果能成,再谈“可靠传输”~~
2. 关注两端的状态:三次握手,也是在验证通信双方,发送能力和接受能力是否正常。
举个栗子~~
A 和 B 要玩双人游戏 -- 双人成行~~~
这种双人游戏,一般都是要双方默契配合的,A 和 B 就要进行一个语言通话,比如 QQ 语音什么的~~~
在进行握手之前,A 和 B 是分别不知道自己的麦克风 和 耳机,是否可以正常工作的~~~
此时,B 就先开口的,通过他的麦克风发出消息:“喂喂喂”
此时,B 是什么都不知道的情况。当 A 能听到“喂喂喂”,就证明了 B 的麦克风是 OK 的,同时,A 自己的耳机也是 OK 的,他就会回复一句”总有儿子想当爹~“
这个时候,如果 B 收到了这一句话,就证明 B 自己的耳机的 OK 的,同时,他也知道了 A 的麦克风是 OK 的(A 和 B 有一个与欸的那个,约定 A 听到 B 相当他爹之后,A 才会回复”总有儿子想当爹“,要不然 A 是不会回复的~~~)由于这个与欸的那个的存在,就证明了 A 一定是听到了 B 说的”喂喂喂“,这个时候,B 就知道了,他自己的耳机和麦克风,A 的耳机和麦克风,都是 OK 的,但由于此时 A 还是不知道剩余的情况,B 就再需要一次回应,讲上述信息告诉 A
当 A 再收到 B 的回复,也就意识到,他刚刚给 B 发的”总有儿子想当爹“(此处还是有一个约定,B 在收到”总有儿子想当爹“的时候,才会回复“我是你爷爷”~~~),B 也收到了,这样 A 也就知道 他自己的麦克风和耳机,B 的麦克风和耳机,都是 OK 的
这样 A 和 B 就都分别知道自己和对方的麦克风和耳机的状态了~~
上述的麦克风 ===》 发送数据的能力
上述的耳机 ===》 接收数据的能力~
3. 三次握手的过程中,也会协商一些必要的参数~通信是客户端服务器两方的事情,双方要进行同步配合,其中的一些内容要保持一致~
举个栗子~~
A 和 B 在玩 LOL ~~~ 第一次握手,B 告诉 A 他玩射手,A 收到后,告诉 B,那我玩辅助,B 又收到后,我们这两个菜鸟选手,玩下路,岂不是一死一送,还需要一个野王带我们飞,于是就又告诉了 A,我们需要一个野王~~~
TCP 中是有很多参数要进行协商的,往往是以”选项“部分来进行体现的~~~
这个选项,我们前面提到过,最少可以是 0 字节,最多是 40 字节(TCP 报头的总长度最多是 2^4 * 4 = 60,去掉前面固定的 20,还剩 40 字节)
其中有一个信息是十分关键的,TCP 通信的序号起始值。TCP 一次通信过程中,序号并不是直接从 0 或者 1 开始计算的。而是先选择一个比较大的数字,以这个数字开头来继续i计算~~ 即使是同一个客户端和服务器,每次连接,开始的序号都是不一样的
涉及 TCP 的大佬当年是这么考虑的,上述操作是为了避免一个情况 ==》 前朝的剑,斩本朝的官~~
比如出现下面的情况:
由于互联网中,会广泛的存在”后发先至“ / “先发后至”的情况。
可能在第一次连接过程中,通信操作中,传输的一个数据报,在路上堵车了~~~迟迟没有到达对端。
等到这个堵车迟到的数据报到达对端的时候,咦,发现已经改朝换代了,之前的连接早都没了,现在成了新的连接了~~~
此时,这份堵车的数据报,应就应该被丢弃掉~~
数据报是按照 ip 和 端口号来进行识别的。
第一个连接,是用客户端 A 来进行连接的,第二个连接是用客户端 B 来进行连接的(恰好是同一个端口的情况,客户端的概率是比较低的(我们前面提到过,客户端的端口号是随机分配的),服务器的概率就会很大)
此时数据到达这一边,发现早已经物是人非了,这个时候的话, 再来对这个数据进行处理的话,就不是很合适了,此时,丢弃这个数据报才是上上策~
那如何识别出,当前的数据报是不是“前朝”的数据报呢?? ==》 通过序号来进行区分~~~
(序号是不同的,也不是随机的,序号的分配背后还是有一些列的分配策略存在的~~~)
正常的数据报的序号都是从开始序号往后依次排序的,就算偶尔丢包,偶尔数据不连续,差异也不会很大,但如果是“前朝”的数据报传过来,序号就会和本朝的数据报的序号差异非常大,很容易一眼就被识别出来~~~
断开连接 - 四次挥手
连接,本质上就是让通信双方来保存对方的信息,每个客户端 / 服务器,都要保存很对对端的信息,一旦多了,这些数据就需要一定的结构来进行管理 == 数据结构~~~
断开连接的本质目的,其实就是为了把对端的信息,从数据结构中给删除 / 释放掉~
此处的四次挥手,就是通信双方,都同意断开连接(双方和平分手,两个人都要去睡觉觉了~~~)(对于如果单方面的情况,四次挥手不一定适用,就会有其他方式会释放这里的连接,也就是说,断开连接,不一定就是四次挥手~)
四次挥手的断开连接操作,就不一定非得是客户端先发 fin 了,服务器也可能先发 fin,和三次握手不一样,三次握手,一定是客户端主动的~
四次挥手中,谁先发都可以,看我们的代码具体的实际逻辑了,调用 socket.close() 就会触发 fin(fin 也是内核中负责完成的,如果进程直接结束,也会触发 fin(关闭 socket 文件,结束进程,也会关闭文件~~~))
fin,同样是可以六个关键字之一,即 finish 的缩写。
当有代码 socket.close() ,就会触发 fin 数据报。
和建立连接一样,断开连接的时候,服务器收到 fin 的时候,同样会发送 ack 和 fin 报文给客户端
然后客户端才传送回一个 ack 表示收到
这样就完成了四次挥手,通信双方各自给对方发起 fin,再各自给对方反馈 ack,四次挥手,代表着双方和平分手,和平离婚~~~(离婚双方需要签署离婚协议 == 》双方都把字签好,就是结束了~~~)
那,上述的四次挥手过程,能不能像三次握手一样,把中间两次的交互,合二为一呢???
如合~~~有的时候能合并,有的时候不能合并,不像三次握手一样,100% 会合并~~
我们再看三次握手的情况:
客户端执行 new Socket(serverIp, serverPort) 代码的时候,就会发送 syn 数据报,中间服务器的 syn + ack 这俩数据的触发时机是完全一致的,都是内核在收到 syn 之后,就立即触发(这两个数据报的发送,和我们程序员写的应用程序代码是无关的~~)
但是四次挥手的过程中,和代码有关系的:
服务器发送 fin 的时候,是受限于代码的,还记得我们在网络编程中,最终在 try - catch 的时候 最后加入了一个 finally 吗?在 finally 的时候加入 close 方法。
这中间返回 ack 和 fin 的时间间隔,如果比较长,就无法进行合并了,只能分为两次来进行传输~~(如果当前时间间很小,也是有可能合并的~)
细节版本:
握手和挥手的相似和不同之处
相似之处:
都是通信双方各自给对方发起一个 syn / fin,各自给对方返回 ack
数据传输的数据: syn / ack / syn / ack fin / ack / fin / ack
不同之处:
三次握手,中间两次的交互一定能够合并,四次挥手则不一定
三次握手,一定是需要客户端主动的,四次挥手,客户端 / 服务器都可以主动~
连接管理过程中涉及到的 TCP 状态转换
上面的 TCP 连接中的详细描述图~~就包含了一些 TCP 的状态转换~
TCP 状态和我们之前线程中谈到的线程,是类似的概念。
状态描述的是某个实体,现在正在干什么。TCP 服务器和客户端都要有一定的数据结构来保存这个连接的信息,在这个数据结构中,其中就有一个属性叫做“状态”,操作系统内核会根据状态的不同,来决定当前应该干什么~~
上图是一个 TCP 状态转换的过程~不过看着有些混乱,我们还是按照那个连接管理的详细图研究一下~
先看一下建立连接的过程~
有一个 LISTEN 状态,表示服务器这边已经创建好 serverSocket 了,并且端口号也已经绑定完成了(手机开机,信号良好,随时可以有人给我打电话了~)
ESTABLISHED 表示已确定的。客户端和服务器已经建立完毕(三次握手完成了)(有人给我打电话了,而且我接通了,此时我们可以给对方说话了~~~)
四次挥手断开连接中:
有一个 CLOSE_WAIT 状态,谁被动断开连接,谁就进入 CLOSE_WAIT 状态~ 这个状态就表示:接下来代码中需要调用 close 来主动发起 fin (收到对方的 fin 之后就会进入这个状态~)
还有的状态是 TIME_WAIT 就是表示,本端给对方发起 fin 之后,对端也给我发了 fin,此时
本端就会进入 TIME_WAIT 状态,给最后一个 ACK 的重传留有一定的时间~(即,谁主动断开连接,谁进入 TIME_WAIT 状态)
正常状态下,CLOSE_WAIT 不太容易被观察到,代码中会比较快速的关闭 socket 这个时候,状态就从 CLOSE_WAIT > LAST_ACK (但是如果发现,服务器 / 客户端上出现大量的 CLOSE_WAIT 意味着代码很可能是出现了 bug,比如忘记关闭 socket~)
TIME_WAIT 存在的意义,主要是为了防止最后一个 ACK 丢包~~
在四次挥手的过程中,会涉及到确认应答和超时重传,如果没有收到 ACK,就会认为丢包~
如果最后客户端发给服务器的 ACK 在传输过程中丢失,服务器没有收到 ACK 报文,就会认为字节给客户端发送的 FIN 未被确认,就会重传 fin 报文段,如果客户端没有 TIME_WAIT 状态,直接进入 CLOSED 状态,就无法处理和接收重传的 FIN 了。而客户端在 TIME_WAIT 状态中,保存着与该连接的相关的信息,可以接收服务器重传的 fin 并且再次发送 ack,保持连接的正常关闭。