【JavaSE-网络部分04】网络原理-传输层:UDP + TCP 可靠性三大核心机制(确认应答 / 超时重传 / 连接管理)

传输层的学习

传输层我们说过最核心的协议是TCP和UDP。

那么在这里面我们再谈一下端口号。

再谈端口号

我们说端口号是用整数表示,用来区分同一台主机上不同的应用程序。

我们前面在网络编程冲每个程序中的socket创建的时候都需要关联端口号,那么对于服务器来说,端口号是程序员的手动指定的;而对于我们的客户端来说,端口号是系统自动分配的。

端口号是由两个字节表示的无符号整数

  • 范围:0~65535。

虽然它的范围呢比较多,但是呢并不是所有的数都能是可以使用的。

  • 0~1023 这样的范围通常我们是不使用的,他们叫做知名端口号,是给一些知名的服务器预留的。

虽然现在我们知名的服务器没有太多,已经寥寥无几了,但是呢有两个知名的端口,一定要重点认识。

  • 80 ==> 这个是给HTTP服务器留的端口号。
  • 443 ==》 这个是给HTTPS服务器留的端口。

问题1一个进程是否可以绑定多个端口号?

答:这个是完全可以的,但是注意其实不是进程绑定端口号,而是我们的socket绑定端口,我们一个进程中完全可以创建多个socket,所以呢可以同时关联到多个端口号,这个呢是一个非常常见的情况。

举个例子:我们在开发服务器程序的时候,一般会提供至少两个端口:

  1. 业务端口:他的是用户的客户端通过
  2. 调试端口/管理端口:这个是不对外公开的,我们程序自己使用,生产环境下服务器程序不能通过调试器来调,可能会把整个进程阻塞住,使用户无法访问。

问题2一个端口号是否可以被多个进程绑定?

答:如果是同一种协议则不行,如果是不同协议是可以的


UDP

我们接下来就看传输层中的UDP协议,学习一个协议,其实重点学习的是协议的格式:那么通过这个协议的格式,我们可以带出协议的很多特性。

整个UDP报头是8个字节,分成四个部分,每个部分占两个字节。

UDP报头每个属性的含义

  • 源端口,目的端口:表⽰数据是从哪个进程来,到哪个进程去;
  • 长度:通过2个字节的数据描述了整个udp数据报的长度(包括报头和载荷),【其中报头固定8个字节长度,所以整个长度-8个字节的长度,就是我们剩下载荷的长度】那么我们2个字节表示的数字最大是65535,也就是64KB,那么一个UDP数据包呢,最大也就是64KB了。
  • 校验和:它起到的作用是验证传输的数据是否出错了。那么我们考虑一个问题,网络传输中为啥会出错?比如我们10101比特流,本质上它是以电信号或者光信号传输的,而我们网络传输过程中,它是会受到外界环境的干扰的,因此呢我们数据往往会出错,如果出错了,我们怎么办呢?
    有两种方案 : 1、发现错误 :UDP的校验和写起到了发现错误的作用【也就是知道数据错误了,但是不知道哪个位错误了】,我们的UDP使用的是发现错误这种方案 本质上是把udp数据报每个字节(除去检验和本身的这两个字节),将它们依次带入到一个数学公式,进行一系列的计算,得到一个数字;然后此时如果输入的这一串字节内容是固定的,并且呢计算公式也是固定的话,此时得到的结果也是一定的;反之,如果发现两次计算的结果不同,那么此时输入的内容也一定是不同的。

    举个例子发送方在发送之前先把这个数据进行计算得到校验和1,然后呢我发送的时候就把数据和检验和1发过去。然后接收方收到数据之后再计算一遍校验和(计算公式呢是和发送方得是提前确定好的),得到新的检验和2,然后他就对比校验和1是否等于校验和2?如果不相等那么数据就一定是错的。

    2、纠正错误,我们用的是海密码,知道数据错了,也能够知道哪个位错了。


那么如果我们的数据超过64KB怎么办呢?

解决这个问题我们方案有两个:

1.我们使用手动实现拆包组包 的这样一个逻辑,但是需要考虑如何拆分,还得考虑如何组合,所以呢相当麻烦。

2.使用TCP代替UDP
计算校验和的方案有多种。

1.最简单粗暴的一种是CRC循环冗余校验,他就是简单的相加。他计算起来简单快捷,但是缺点相对来说容易出现输入不同数据得到相同结果的情况。

2.更常见的方案是md5系列/sha1系列,这样的方案它的特点:1)定长:即无论你输入的内容是多少个字节,他输出结果就是固定长度的。2)分散:输入的内容哪怕只改变一点点,最终算出来的结果都会差别很大。3)不可逆:根据原始数据计算出md5或者sha1是非常快的,然后呢再根据md5或者sha1的值反推出原始数据的成本会非常非常大,几乎不可能。这样的方案也经常会在加密领域中使用。

UDP更主要的应用场景对效率要求高,丢包概率低的情况。比如我们在同一个机房内部的机器之间通信。


TCP

TCP的报文格式:

具体的图:

属性:

16位端口号和16位目的端口:表⽰数据是从哪个进程来,到哪个进程去;

4位首部长度:他描述一个TCP报头的长度,TCP数据报=报头+载荷,那TCP报头长度是可变的,导致需要有一个属性来描述它到底有多长,这个属性我们叫做选项option,它是可选的,可有可无的,选项option中有很多个属性,可以选择一个,也可以选择多个,还可以一个都不选【也就是说我们的TCP报头是可变的,原因就是他有了选项这个属性】。

我们从上面的图中是可以看到的,这个4位首部长度是4bit,那4bit他是0~15的一个位数。在我们tcp的报头中,除了选项这样一个属性之外,固定部分长度总共是20字节。那么如果我们再算上这个选项的话,肯定用这15个比特是不能表示的呀。其实tcp他有一个独特的技巧,他在表示报文长度之时,不是使用字节为单位。而是使用"4个字节"为单位,比如tcp4比特长度选项中写了长度为15,那么实际上真实的长度是60,就是4×15=60【4比特最大为15,而以4字节为单位,因此呢报头最大为4×15=60个字节】,而我们固定长度占了20个字节,因此我们说报头中选项属性部分最多占40个字节,且由于爆头的长度是4个字节为单位,因此呢添加选项属性中值的时候我们是以四的倍数去添加的。

保留位:他的意思是现在不用,但是留给未来做扩展,他呢是吸取了udp不够用的一个教训:我们前面学udp可以知道,udp有一个问题,就是长度不够的话是不可扩展的,若要扩展会相当麻烦,因此tcp的设计者就考虑了这样的问题,所以呢我们tcp设计者在报头中预留了一些保留位,即我现在先不用,我就占个位置,指不定后面呢就有用了。

6个标志位 :它是TCP中最重要的6个标志位。

16位校验和:和前面udp的校验和是一样的,也是用来检验数据是否出现错误。发送端填充,CRC校验.接收端校验不通过,则认为数据有问题.此处的检验和不光包含TCP⾸部,也包含TCP数据部分.

以上是我们现在能够认识的一些属性,那么对于6个标志位分别什么意思,32位序号,32位确认序号,16位窗口大小,16位紧急指针,选项等等这些属性我们所有后续会慢慢讲,因为呢它需要结合tcp的一些机制来解释,我们在说tcp的核心机制的时候会带动这些属性学习。


可靠性

TCP最核心的机制就是可靠性。

我们在网络通信中可能会存在丢包的情况,我们就是针对于丢包这样的一个情况才去谈可靠不可靠。

那网络通信中我们为啥会丢包呢?

比如堵车这样的一个情况,那么我们为什么会堵车呢?其实啊是因为一段路线中单位时间内能够通过的车的流量是有上限的。一旦超过了这个上限,我们就会堵车。我们的网络世界也是如此,我们主机与主机之间并不是一根网线直接相连的,而是中间有很多很多的设备构建了我们的网络路径:

一个数据发送出去,到对方接收到,中间是经过很多的路由器或交换机的,这些路由器或交换机就相当于我们的路口,那每一个路由器或者交换机它的转发能力也是有上限的,所以此时如果我们在某个时刻数据转发量暴增,那么就可能超出某个设备的转发能力上限,从而呢会导致数据丢包。而我们的网络世界中,数据是有时效性的,如果数据出现了堵车,那么最终我们到达目的地可能就无意义了,还不如直接丢了【因此丢包通常是路由器或交换机的一个主动行为】。

丢包问题UDP和TCP的处理方式:

  • UDP的处理方式是数据发送出去之后就不管了,这种称为不可靠传输。
  • 而我们的TCP处理方式是数据发送出去之后呢,要关心对方是否已经收到,如果没有收到还会有补救的措施,这种我们称之为可靠传输。

所以丢包仍然是客观存在的,并且呢无法预知啥时候产生丢包,因此我们的可靠传输是为了对抗丢包的,怎么对抗?

  1. 感知到丢包
  2. 如果丢包了能够做出补救。

我们说TCP是可靠传输,那么TCP如何实现可靠传输的呢?这个就是我们TCP核心机制所要来完成的事情,我们首先来看核心机制1:确认应答。


TCP核心机制1,确认应答【实现可靠性基础1】

TCP的核心机制一确认应答,它是实现可靠传输的核心机制。

这里呢我们补充:我们网上会有资料说TCP实现可靠性,主要是靠三次握手,这样的说法呢其实是不太对的,那么具体呢,我们会在后续的机制三次握手中解释。

那么什么是确认应答呢?

就是我们怎么能够感知到对方是否收到我们的消息呢?

我们只需要对方收到消息之后呢,要回复一个收到就可以了。

那么我们怎样知道对方收到了呢?哎,就需要对方返回一个应答报文(acknowledge)简称ACK,发送方知道应答报文之后呢就可以确定对方已经收到消息了。

于是此时我们就说到了我们6个标志位中的第二位:ACK

举个栗子

我想要请女神去吃饭,然后呢我就用手机给女神发了一个短信

eg1:简单模型

我:女神,女神,我请你去吃火锅吧。

女神:好啊好啊!【应答报文】


上述这样的简单模型其实是有问题的。

比如:如果我发送多条消息

eg2:

我:女神,女神,我请你去吃火锅吧。
我第一条刚发送过去,我就立马发第二条了

我:女神,女神,你做我女朋友吧。

此时在女神这边呢就收到了两条消息,于是呢她就回复两条。

女神:好啊好啊!【应答报文】

女神:滚

此时呢我就根据女神回复的消息的顺序明确他的意思。此时我们就会面临两个情况:

1.如果处理顺利的话,我就能正确的收到女神对应回复的消息。


2.如果处理不当的话,就会出现除了丢包之外的一个情况,叫做后发先至,如图:

此时呢我收到的肯定是先收到滚,后收到好啊,好啊。于是呢就代表着女神同意做我女朋友,但不同意去吃火锅,这不就让我理解错误了吗?


所以后发先至,会影响我们的理解,那么关于后发先至为什么会出现这样的一个情况呢啊?

咱们就举一个例子:

咱们结婚了有一个环节叫做接亲,新郎在婆家,新娘在娘家。我们一大早天没亮,新郎这边呢就会组织一大帮车队啊,带着他的伴郎团队准备去新娘家去接亲,如图所示啊,假设我们的新郎是在头车里面,我们万事俱备,只欠东风,ok,我们这些车队大家一起出发,同时出村,但是呀由于好多好多的原因呢,比如等红绿灯:新郎的头车突然遇到了红绿灯,但是在走的途中呢,后车伴郎他们的车呢是比较快的,他们过了红绿灯,于是啊我们走着走着,哎,这些车队呢会出现走散的一个情况,于是最终我们到达的目的地是相同的,只不过我们到目的地的顺序就不一样了,于是就可能出现后面的候车比头车要先到达新娘家。

因此呢在我们网络世界中也是类似这样的一个情况,我们在转发数据的时候,我们发的多个数据包就是一辆一辆车,经过的每个路由器或者交换机就等同于十字路口,每个数据包在经过路由器转发的时候,走的路径可能是不同的。因此呢出现了典型的后发先至,如图所示:

那么针对这样的一个场景呢,我们TCP如何处理?

对于上述这样的情况,首先我们应答当然是需要的,只不过我们得考虑应答的顺序。那如何做到给应答保持顺序呢:我们给数据编号。

此时啊对于我们的应答数据呢就可以根据我们这些数据的编号完成顺序应答了。

比如我们上述中:

女神,女神,我请你吃火锅吧【我们编成1号数据:1.女神,女神,我请你吃火锅吧】

女神,女神,你做我女朋友吧【我们编成2号数据:2.女神,女神,你做我女朋友吧】

于是女神的回复就是:针对2滚;针对1好啊,好啊。

最终这两条应答哪怕在我收到的时候顺序呢是不对的,但是我能通过编号来得出它是对应哪一条请求的回复。

这里我们举的这个例子其实是比较简单的,它呢是按照条(一条一条的数据)去编号的。但是我们真实的TCP并非如此,我们TCP是使用字节流的,TCP实际上是针对每个字节进行编号的:用连续递增的整数来对数据进行编号。

那么由于我们的编号是连续递增的,所以我们只需要知道数据中第一个字节的编号,然后后面每个字节的编号我们自然就可以得出来了。

那么这样的一个编号是在哪里记录着的呢?

此时就谈到我们前面会说的一个属性,tcp报头中的属性:32位序号。

注意:我们的编号是给TCP的载荷部分编号的,而一个TCP的载荷是由多个字节构成的。可是TCP报头中的32位序号只能写一个数字呀,那么请问我们写哪个数字呢?就是说呀,我们的载荷是由多个字节构成的,那这多个字节就意味着有多个编号,而我们的32位序号这个属性里面只能填一个,请问是填哪一个呢?

答:我们填的是第一个字节的编号,因为只要知道第一个字节的编号,那么后面字节的编号我们自然就可以得到。

注意:他是给载荷部分编号的,不是给报头编号的,保存的就是当前数据包载荷中第一个字节的编号。

然后我们的应答报文要根据这样的序号来进行应答。

假设我们发送方案的序号从1开始是1~44号的数据。那么我的接收方收到这些数据之后,就要返回确认序号ACK,用于告诉发送方收到了哪些数据。

那么我们确认信号是怎么填写的呢?这个确认序号在哪里填写的呢?

1.填写在哪里此时我们就谈到我们TCP报头中没有说到的那个属性,32位确认序号,我们的确认序号就是写在这个位置。

2.怎么填写:他的填写规则是把收到的数据载荷的最后一个字节序号加1填写到确认序号中。这样的好处呢我们要后面才能体会到,我们到时候会说。

也就是说应答报文中的确认序号填写的值是收到的数据最后一个字节的序号加1,而不是第一个字节的序号。

你可以理解成:

1.接收方告诉发送方哪个序号前面的数据全部收到了

2.接收方也是向发送方索要下一个数据从哪里开始

好比我们刚才发送1-44的数据,那么我们接收方返回的序号应该是45,告诉发送方1~44的数据我已经收到了,你下一个要发送序号的位置,从45号开始发。


再举一个例子:

发送方:发送1~1000的时候,此时TCP是一个普通的TCP数据包,此时序号这里填写成1,ACK标志位为0,
接收方:返回应答报文1001的时候,此时TCP是一个应答数据报,此时序号这里独立编号 【独立编号的意思就是说你发送方和接收方他们各自编各自的编号。就是两个是互不影响的,我接收端不可能跟在你那个发送方后面去编号的,我接收方编号我自己的】,确认序号这里填写的是1001,ACK标志位是1,同时ACK数据报的载荷是空的 【目的只是确认 1~1000 字节收到,不需要发任何数据,所以载荷为空 ------ 这是 TCP 里最常见的 ACK 形式】

此时我们怎样理解呢?首先我们发送方拿到确认应答是1001,那它就代表小于1001的数据都已经确认收到了,同时呢还代表接下来你要从1001开始给接收端发送数据。


注意1:序号和确认序号都是针对载荷的,TCP报头不参与编号。

注意2 :独立编号的意思就是说你发送方和接收方他们各自编各自的编号。就是两个是互不影响的,我接收端不可能跟在你那个发送方后面去编号的,我接收方编号我自己的。画个图解释一下:


【注意哦】:我们的应答是acknowledge,响应是response;他两个是不一样的东西哦,响应是带有业务的数据,不要将他们混在一起。

也就是说你客户端给服务器去发送一个请求,那么我一定会给你一个应答的,但是我给不给你响应,那你看你的业务代码怎么写?因此呢在这样的场景下,我们编号是分别编号,也就是独立编号,就是说但凡是客户端给服务器发送的数据,它是一组编号;然后呢我服务端给客户端访问的数据,它也是一组编号。


我们在考虑确认应答的时候,我们是没有考虑丢包的情况的,但是这样的情况实际是存在的,那么对于丢包的情况我们怎么处理呢,我们就得考虑到我们的TCP的核心机制二,超时重传。


TCP核心机制2,超时重传【实现可靠性基础2】

确认应答呢是实现可靠传输的核心机制,而我们的超时重传是针对确认应答进行的重要补充【就是说确认应答是一般情况,而我们超时重传是对确认应答出现丢包的一种特殊情况处的理】,他也是实现可靠传输的机制。

确认应答和超时重传一起就实现了我们TCP的可靠性。


我们说丢包是客观存在的,它是随机出现的,我们无法预测什么时候可能会出现丢包,但是我们能够在出现丢包的时候能够做出一些事情,做什么事情呢?就是如果丢包了,那么把丢的数据重新发送一遍。

我们重发数据是能够对抗丢包的,丢包是概率事件:

假设某个系统中丢包的概率是10%,那么一次传输中数据丢包率是10%,如果两次连续传输丢包了,那么概率就是10%乘10%等于1%。是的,所以呢也就是说你多传输一次过去,他成功的概率从90%变成了99%。故重传它是可以提升我们成功的概率。


那么我们如何识别当前数据丢没丢呢?

我们根据等待超时时间来判断的。

也就是说如果你的发送方收到了确认应答ack,那么就代表接收方收到了你的数据。而如果你的发送方没有收到ack,此时对方你能知道他有没有收到吗?不好说,我们先等一会,看一看,等待一段时间再判断。

我们在这个特定的时间间隔,这个时间内都没有收到ack,就认为丢包了。

我们数据丢包有哪一种情况呢?有两种情况:

`对于我们的发送方来说下述两种情况他都只能一视同仁都去重传,因为我们区分不了这两种情况。`

1)A给B发数据丢了:对于此种情况呢,我们主机B本来就没有收到1~1000的数据。所以呢你主机A重新发送就好了,如下图

2)B给A返回数据ack丢了:那对于此情况呢,我们主机B已经有了1~1000的数据了,然后你再重新发送一遍,导致我们B收到两份一样的数据,那么你觉得这个合理吗?具体的解释我们写在下面。

对于情况2的话,我已经收到1 ~ 1000的数据了,然后你重传1~1000,此时呢他就收到重复数据了,我们怎么办呢?我们不做任何处理吗?它确实是有问题,如果TCP不做处理,那么可能会是应用层读了两次一样的数据。这样是很危险的,比如我们的扣款数据,你不可能扣两次吧。因此呢针对这样的情况我们的TCP会在内部进行了一个去重操作,我们TCP的接收方会根据收到的序号进行去重,从而呢保证同一份序号的数据在应用程序这里只能read一次。

如何去除呢?

我们接收方在操作系统内核中有一个接收缓冲区。这个接收缓冲区啊是在我们内存中的,我们每个socket都有自己独立的接收缓冲区,这个缓冲区你可以把它想象成一个队列。我们接收方每次收的数据都会把它放到这个队列中,然后呢每次收到新的数据,操作系统都会判定当前这个序号是否已经在缓冲区中存在的。诶,如果存在就直接把这个数据给丢弃了,如果不存在,我再放到接收缓冲区中。然后后面我们应用程序读数据也是从接收缓冲区中去读取。

所以从这里我们可以看出TCP通信本质上也是生产者消费者模型。我们所谓的接收缓冲区就好比是一个阻塞队列。

数据在接收缓冲区中有两个作用:

  • 第一个去重
  • 第二个重新排序

这样不仅能保证我们接收方收到的数据是不重复的,而且呢也能保证应用程序得到的数据就是发送的数据,从而解决了后发先至的问题。

好比我们的之前举的那个例子去接亲,对吧?还有我们到达娘家的那个村口啊,我们就在那等,等着给新郎的头车进来之后我们再排好队再进村。

那如果你用UDP去实现这些东西的话,我们说UDP,他的数据可能会超过上限,然后呢你就得拆包,所以你就得自己写代码去实现这些等待,重传,排序等等过程反正成本很高。


这里呢我们说通过超时时间来判断是否丢包对吧?那么超时时间的设定等多久合适呢?在我们的TCP中,这样的超时时间是一个动态变化的值,因为在我们网络世界中,如果你的网络很通畅的话,诶,它过一会就回来了;但是如果你的网络很拥堵的话,你就得很久,对吧?所以呢它是动态变化的,如果你把这个时间写固定写死的话,那他就无法契合这两种情况了。

那他怎么做的呢?他的核心思路就是我们每次重重传的时候会扩大超时等待的时间:比如我发送一个数据,对吧,然后我等了一下。诶,发现还不行我就进行第一次重传,我再等一下,把那个等待时间给延长,发现还是没来,我就进行第二次重传,然后我再继续延长等待时间;等待过后还是没来,我就第三次重传,延长等待时间,每一次的等待时间都会增加,也就是延长时间的阈值t0<t1<t2<t3的,如下图:

最后如果我收了这个ack了,那么此时就没有超时重传了,我们就收到ack,

但是注意我们重传的等待时间不是无限长的,重传的次数也不是无限多的,我们是有一定目标阈值的。我们达到一定程度就会视为网络出现了严重的故障,此时我就不传了,管你的。直接放弃通信,也就是释放掉连接。


超时重传和确认应答都是TCP实现可靠传输的核心机制。


TCP核心机制3,连接管理【辅助可靠性传输】。

连接管理是辅助实现可靠传输的

我们连接管理,就做两件事儿:

  1. 建立连接
  2. 断开连接

这里呢我们再回忆一下什么是连接?咱们在网络上的连接是指抽象的逻辑上的问题,他呢并不是拿一根绳子两个主机给连起来。而是通信双方各自保存了对端的信息【核心保存的是IP和端口】。

那所谓的建立连接呢就是通信双方保存对端信息这样的一个过程。

而断开连接的就是通信双方删除对端信息的一个过程。


建立连接

建立连接的工作过程称为三次握手

那么什么是握手呢,他的单词叫做hardshake,它是我们计算机中常见的专业术语,在我们的现实生活中,握手其实本质上就是打招呼,那么握手这样的一个动作,他本身是没有实质性的业务数据的,我们只起到一个打个招呼的作用,因此呢我们将它引入计算机之中。

那在我们网络中什么叫做握手呢?就是说我发送一个不携带业务的数据,通过这个数据和对方打个招呼【仅仅是打招呼】,换句话说,我们握手过程中传递的是"只有报头,没有载荷"的TCP数据包。


那么三次握手是怎么个握法呢?如图所示:

三次握手首先是由客户端主动发起。注意,别忘了客户端之所以叫客户端,因为他是主动的一方【谁先发起第一次请求,谁就是客户端】。

1.我们说握手,他呢只有报头数据,没有携带载荷数据,我们客户端给服务器发的这个请求中,他的报头里面,包含了一个SYN标记。

这个SYN标记呢就是我们TCP报头中6个标记位中的第5位

SYN它是synchronized的一个简写,译为同步。这个synchronized在我们多线程中也出现过,然后我们此处的呢也是synchronized的一个简写。那么虽然他们都表示同步,但是呢他们的含义是不同的。也就是说在我们计算机中,一个专业术语经常会有多种含义,我们得结合上下文来理解。在我们多线程的加锁的同步中呢它本质上是互斥;

而在我们TCP这里的同步其实是一种通知:那意思就是说诶,接下来我要和你进行通信了,我这个同步就是代表我要把我的信息同步给你【比如你在学校里的辅导员要开会,刚好到你的同学小李呢请假了,那么会后,辅导员会告诉你,你把今天会议的内容同步给小李说一下】。

2.此处这个SYN数据包呢,我们也称之为同步报文段。

3.此时服务器会给我们返回一个响应,这个响应就是我们前面所谈到的ACK【注意哦,我们的确认应答,不是在发送消息后才会有的,我们在建立连接之初,确认应答也是存在的】,返回ACK代表当前我这个消息呢被服务器给收到了。也就是说呀,服务器对客户端说你要给我建立连接,那么,我回复你客户端:好的。

此时其实就完成了这件事:客户端说我要跟你建立连接了,所以我客户端要保存你服务器的信息了,服务器就来了一个好的。

4.而建立连接是双方的事情,一个巴掌我们是拍不响的,我们要两个巴掌,类似于我们结婚领证一样,双方都要有一个结婚证。

我们此时完成的,其实就是 TCP 三次握手的核心过程:首先,客户端主动告诉服务器:我要和你建立连接,我会保存你的信息;服务器同意后,客户端就会记下服务器的信息。与此同时,服务器在返回确认应答(ACK)时,还会向客户端发送同步报文段(SYN),意思是:我也要和你建立连接,我同样需要保存你的信息。最后,客户端收到这个同步报文段后,会再返回一个 ACK 给服务器,表示:我知道你要保存我的信息了,已确认。

到现在为止,我们的通信双方呢把对方的信息都保存好了,那我们的连接才算完成。


举个栗子:

比如我们的结婚,我们的那个婚证人啊,他会问新郎,你是否愿意娶新娘啊?那新郎说我愿意,然后呢那个婚证人呢再问我们的新娘,你是否愿意嫁给新郎呢?那新娘说我愿意,只有双方都愿意了,我们这个婚礼才能举办下去,如果有其中一方说我不愿意,那我们肯定是无法进行结婚的。

我们的建立连接也是如此。若通信双方有一端不愿意,我们的连接都建立不起来。


那我们上述的图呢其实是不太标准的,你们不是说三次握手吗?那这里为什么会有四次呢?其实原因如下。

中间那两次我们合并在一起了,为什么要合并在一起呢?

首先中间两次的数据传输,它们的时机完全相同的

第二个原因是我们把两个数据合并成一个,它能够提高我们的传输效率。

那么我们为什么会提高传输效率呢这里边?这里就提到了我们前边学过网络通信整个过程中的封装和分用,如果我们合并成一次发送的话,我们就只需要封装和分用一次;而如果我们分两次发送的话,就需要封装分用两次。

所以此时我们这样的一个数据包里面我们将标志位位SYN和ACK都设为1


我们上述解释完三次握手是怎么握的之后那么我们思考一个问题:
三次握手之意义是什么?他能够解决什么问题?

1.第一个意义:它能够起到投石问路的效果,初步验证了网络通信路径是畅通的

因为你只要验证出这个网络通信是通畅的,那么他就为我们后续进行可靠传输做了一个前提条件(这也就是说连接管理是我们可靠传输的一个辅助功能的原因)。

比如啊我们修一个地铁:那你修好地铁之后,你可以直接去跑吗?我们可不敢,我们一般会拿一个空车先去跑一遍,如果你这个空车呢跑通了,就代表我们这个地铁修的就没问题。

所以呢对比我们这里边的连接,我建立连接,我只是握手,去探一下路,我不携带任何数据,我只是去看一下路,后面如果他通的话,你就将数据携带在载荷中传过去。

2.第二个意义:他能够验证通信双方的发送能力和接受能力是否正常?

他这个什么意思呢?

我们举这样一个例子:我们放假回家了,和自己的室友呢也就各回各家了,那么我们有一天在一起开黑打游戏,我们打什么游戏呢?我们打王者荣耀,于是我们为了方便沟通,就把王者荣耀的语音麦克风给打开用于沟通交流,所以我们在开游戏之前会检查我们的耳机和麦克风是否正常工作。

第一次由 发出语音,我说:听得见吗?听得见吗?

然后我的室友 呢,他听见我的语音了,此时就可以验证:我这边麦克风是正常的,然后室友的耳机是正常的

然后我室友 那边呢就反过来问我:我听得见,你听得见吗?你听得见吗?

此时 这边能够听到了是有"你听得见吗?"这个语音,此时就可以验证了:我的耳机是正常的,我室友的麦克风是正常的

这里还要注意哦,我们是约定过的,只有我的室友听见我发的"听得见吗?",他才会回复我:我听得见,你听得见吗你听得见吗?所以到此时在我的这边, 也就知道了:我室友耳机和麦克风都是正常的啦,同时,自己的耳机和麦克风也是正常的。但是我室友那边他只知道我的麦克风正常的以及他的耳机是正常的,他并不知道我的耳机是否正常。

因此呢我们就得第三次通信,目的是把我掌握的设备正常这些信息也告诉我的室友。

于是第三次通信 就发送:正常正常,一切正常。此时我的室友收到之后呢,他就知道了,我的室友刚才发送的 "我听得见,你听得见吗?" 已经被我收到了。所以呢此时我的室友也自然就知道了:我室友自己的麦克风是正常的,然后我的耳机也是正常的,从而呢我的室友就知道我设备正常的信息。也就是说我室友知道了我室友的耳机,麦克风都正常,以及我的麦克风,我的耳机正常

这里边的耳机就代表接受能力,麦克风就代表发送能力。

3.还有第三个意义:三次握手过程中可以完成关键参数的协商。

什么是参数协商呢?我们用办酒席的例子一讲就懂。

办酒席前,总得先商量好:要摆多少桌,才能让所有亲戚朋友都坐得下?

我先和家里长辈商量,长辈说:远方来的亲戚大概要准备 20 桌;我再回复:本地的邻居、朋友大概 17 桌;最后长辈两边合计,敲定总共准备 40 桌。

这个双方互相沟通、一起敲定细节的过程,就是 TCP 里的参数协商。三次握手不只是 "确认能连上",还会在这个过程里,客户端和服务器互相商量好后续通信要用的关键参数,为后面稳定传数据做好准备。

那我们做参数协商的这个过程其实是在我们TCP报头的属性选项部分去填的。

疑问:我们三次握手中协商的一个重要参数就是序号从哪里开始?

也就是说我们TCP通信时序号一定是从1开始吗?这是不一定的。从哪开始?我们是可以通过参数协商这样的一个作用去调整的。具体从哪开始呢?其实就是在我们三次握手的时候,客户端和服务端各自生成自己的一个初始序号,再通过三次握手去告诉对方,然后对方就知道接下来咱们的通信,你的数据会从哪个序号开始了。
为什么咱们不直接从1开始而是要搞一种这种随机的数据?

答,我们不从1开始是为了防止一种特殊情况:前朝的剑斩本朝的官。

如图:比如有一次我们的客户端给服务器发送数据,但是很遗憾,我们这个数据它迷路了,也就是说它绕了一个很远的弯路,然后还堵车了。导致他一直到不了我们的服务端,在他没有到达服务器之前啊,可能我们的客户端和服务器就已经断开连接了。那后面我们又重新建立连接了,这个迷路的数据,兜兜转转它又跑到服务端了,但是呀你当前的连接已经不是之前的链接了,而你这个数据是之前连接发的数据。那问题来了,你当前连接收到的这个数据是之前连接所发的,这样的数据你要不要? 这样的数据我们是不要的,我们应该丢弃掉,因为我们重新建立连接之后,有一种可能就是我们本次通信所建立的连接程序和之前所建立的连接程序并不是同一个程序。

那这里边我们如何识别这个数据是上次连接而不是当前连接的?

我们通过序号来判别,也就是说我们每次建立连接的时候协商序号的目的,即我们每次建立连接都相当于随机数字作为起始序号,此时我们就会出现上次连接和本次连接的序号就会差别很大。通过这个现象,如果接收方看到有一个序号呢和大部分序号差别特别大,哎,就能判别出他是上次连接了就把它丢掉。

这就是我们说不能都从1开始防止你看不出这样的一个差距【即防止你那个上次连接的序号和本次大部分数据的序号没太大差距】


🤔:三次握手为啥要经过三次,四次是否可以?两次是否可以?

四次是否可以呢?我们认为是可以的,但是没必要。我们中间了两次通信,最好呢还是合并在一起进行,不要拆开,如果你拆开的话,他效率就会降低。

两次是否可以呢?我们认为是不可以的,我们说三次握手有一个重要的目标就是验证通信双方发送能力和接收能力是否能够正常。如果是两次的话,我们没法达成这个目标的。


三次握手中的TCP状态变化图:

我们按照如图所示中的序号来解释状态

状态:

在多线程中,我们线程的状态是给我们调试提供了参数依据。

因此我们此处tcp中三次握手,它的状态也是起到这样的一个作用:就是为调试提供参数依据。

  • closed:它呢是一个虚拟的状态,表示tcp连接是没有的
  • listen:他表示服务器进入了专属状态,就是说我们服务器已经准备就绪,随时可以用客户端来建立连接。
  • syn_send:客户端发送出第一个syn处于的状态。
  • syn_rcvd:服务器收到第一个syn处于的状态。
  • syn_send和syn_rcvd这两个状态他们存在的时间是特别短的正常情况下我们是看不到的。但是如果你的通信存在问题,此时你就可以看到这里的状态了,所以呢,当你肉眼能够看到这两个状态,就说明你三次握手应该是出现了麻烦。
  • established:他代表连接已经建立好了,接下来就可以进行数据通行。好比就你电话已经打通了,接下来就可以说话了。

断开连接

断开连接,我们用的是4次挥手,他也是一个不携带载荷,没有业务意义,表示特定功能的TCP数据包。

我们三次握手,一定是客户端开始第一个操作的。开始第一个操作的一方,我们称之为客户端。

而对于四次挥手来说,客户端和服务器都有可能是主动发起的一方。

我们假设我们客户端呢先主动发出吧,那么客户端想给服务器说,哎呀,我要和你断绝连接了,就是说我打算把你的信息给删除了。此时客户端就会向服务发送一个信息叫做FIN,FIN是我们结束报文段,它呢是6个标志位中的第6位。

此时我的服务器就会返回一个ack,就是说服务器说哎,你要把我删了,好,可以。与此同时呢,服务器就说你要删除我的信息,你要和我断开连接,那么我也要把你删除,我要将你的信息给删除掉。

最后客户端说,哎,你服务器要把我的信息删除,可以可以的。客户端回复一个可以,之后呢双方就分别加对端的关键信息给删除掉,此时我们的连接就断开成功。


哎,看到这里细心的老铁可能会发现我们三次握手中,中间那两次是可以合并的。那么对于我们的四次挥手,可不可以将中间两次也能合并变成三次挥手呢?这个不一定哦!!!有时候是能的,有时候是不能滴。

先解释为什么不能:在我们标准情况下它是不能合并,我们不能合并的关键原因是在于服务器返回ack和发送fin的时机是不同的,服务器返回ack是在服务器收到fin后由操作系统内自动的立刻就返回了,而fin则需要应用程序执行到close方法,等进程退出之后才会出发。这两者之间它是有时间差的。

为什么能:触发特殊机制情况下能【后面会学一个机制:捎带应答,捎带应答下fin和ack可能会达到同一时机】


四次挥手状态变化图:

按照图中的序号解释状态:

  • fin_wait_1和fin_wait_2和Last_ack这几个状态都会很快消失。
  • close_wait:它是在等待执行你的应用程序代码来调用close方法。
  • time_wait:谁主动断开连接,谁就会进入到这个状态。还有一个特点就是我们等待一定时间之后再释放连接,进入到这个状态之后呢,正常情况下我们收到也不会发送任何的数据了,我们就静静的等待一段时间连接就释放了。

那么为啥要等一会再释放?为什么不直接释放掉呢?

如图所示,我们的关键要点是最后一个ack可能会丢包,如果是前面的数据丢包的话,那么直接触发超时重传机制就可以重传,此时通讯双方的连接都正常的,你可以重传。

如果客户端在发送最后一个 ACK 报文后直接释放连接,一旦这个 ACK 报文丢失,那么此时对方服务器因为没有收到ack从而服务器就会重传这个fin,而由于你的客户端这边已经把连接给释放掉了,所以呢就没有人能够响应这个fin的数据,也就没有人能够返回ACK了。因此呢我们这个time_wait这个状态呢就可以说是你等待一段时间再释放连接,这样就能解决上述出现的问题,也就是说你这个状态平时呢也没啥作用,但是真出问题了,你是能解决它的。
那么我们说time_wait是等待一段时间的,那么等待多久合适呢?我们说要让他等到一个比较充分的时间,要让它存在的时间足够对方进行FIN的重传,确保一定是对方不会重传了,我们再释放。那他这个状态时间是可以设定的,我们操作系统中有一个参数就是做MSL,他表示网络通信中从一端到另一端传输经历的最大时间,time_wait等待时间是2MSL。


好啦,UDP 和 TCP 的前三套核心机制就聊到这~TCP 一共有 10 个核心机制,下一期咱们重点讲第四、第五、第六个 ------ 滑动窗口、流量控制、拥塞控制,看看 TCP 是怎么在保证可靠传输的前提下,把性能也提高的!老铁们别忘了点赞👍、关注、加收藏,咱们下期见👋~

相关推荐
运维闲章印时光2 小时前
企业跨地域互联:GRE隧道部署与互通配置
linux·服务器·网络
xixixi777772 小时前
太赫兹通信和可见光通信的区别对比
网络·应用·信号·无线·通信·太赫兹通信·可见光通信
加农炮手Jinx2 小时前
Flutter for OpenHarmony 实战:Injectable — 自动化依赖注入大师
网络·flutter·华为·harmonyos·鸿蒙
z10_142 小时前
住宅代理IP是什么?如何利用住宅代理做海外营销
网络·网络协议·tcp/ip
匀泪2 小时前
云原生(Keepalived实验设定)
服务器·网络·云原生
天荒地老笑话么2 小时前
NAT 下代理最佳实践:HTTP(S)_PROXY/NO_PROXY
网络·网络协议·http
通信大师2 小时前
Cat-M技术详解:5G前行中的低功耗广域网络之星
网络·5g
没有bug.的程序员2 小时前
容器网络深度探究:从 CNI 插件选型内核到 K8s 网络策略安全防护实战指南
java·网络·安全·kubernetes·k8s·cni·容器网络
安科士andxe7 小时前
深入解析|安科士1.25G CWDM SFP光模块核心技术,破解中长距离传输痛点
服务器·网络·5g