zl程序教程

您现在的位置是:首页 >  数据库

当前栏目

传输层协议——TCP协议(重点)

2023-04-18 14:22:54 时间

❣️关注专栏:JavaEE

与之相关的协议:UDP协议https://blog.csdn.net/qq_58299006/article/details/129391701


🍥 1 TCP 协议段格式

(1)选项

option(选项)此处的选项相当于对这个TCP报文的一些属性进行解释说明的。

(2)4位首部长度

一个TCP报头,长度是可变的,不像是UDP是固定8个字节的。因此首部长度就描述了TCP报头具体有多长,另外选项之前的部分是固定的长度(20字节),首部长度-20 得到的就是选项部分的长度,注意的是此处的首部长度是 4bit 位,首部长度的单位是 4字节,不是字节。eg:如果首部的长度值为5,那么整个TCP报头是20字节(相当于没有选项);如果首部长度是15,那么整个报头是60字节,选项就是(60-20=40字节)。

(3)16位校验和——原理和UDP的校验和原理相同。

(4)保留(6位)

保留(resevered),比如在C语言中有一类单词叫做关键字,除了关键字之外还有一类单词就叫做保留字。保留字现在还没有开始使用,但是说不准后面某一天就会用到,所以在这里先用保留占个位置,此处TCP的保留6位也是为了以后的扩展来考虑的。对于网络协议来说,扩展升级是一件成本极高的事情,如果引入了“保留位”,此时的升级操作的成本会低很多,如果后续引入了一些新的功能,就可以使用这些保留位字段,这对于TCP报头结构的影响是比较小的,旧的设备即使不升级也能很容易兼容。

🍥 2 TCP 特点

有连接、可靠传输、面向字节流、全双工

🍥 3 TCP 内部机制

TCP是一个很复杂的一些,里边有很多的机制,这里主要讨论TCP提供的10个比较核心的机制。TCP对数据传输提供的管控机制,主要体现在两个方面:安全和效率
这些机制和多线程的设计原则类似:保证数据传输安全的前提下,尽可能的提高传输效率

🍥 3.1 确认应答(安全机制)

确认应答机制是实现可靠传输的最核心的机制!TCP是可靠传输,那么到底怎么实现可靠的呢?这里的可靠不是说发送方100%的就会把消息发送给对方(如果网线断了那么怎么能100%传输呢),而是尽可能把数据传输过去,如果传输失败,至少能知道是传输失败了。

接下来通过具体的例子讲解以下确认应答机制: 

(1)简单情况

TCP进行可靠传输,最主要就是依靠确认应答机制。A给B发送了消息,B收到之后就会返回一个应答报文(ACK),此时A收到了应答之后,就知道刚才的数据已经顺利发给B了。生活中的打电话就相当于是可靠传输,只有对方接听之后你们才能进行交流。

(2)复杂情况 

结论:网络的先发后至现象是客观存在的,无法避免,因此应答报文到达的顺序可能发生变动,此时就需要考虑如何规避这种顺序错乱带来的歧义。

其实很简单,只需要给传输的数据和应答报文进行编号就可以了。 

实际上TCP的序号不是一条一条这样的方式编号的,TCP是面向字节流的,序号也是按照字节来编号的。

TCP的字节的序号是累加的,每个TCP数据报填写的序号只需要写TCP数据报头一个字节的序号即可,TCP知道了头一个字节的序号,根据TCP报文长度,就很容易知道每一个字节的序号了。

前1000个字节的序号是1,那么第二个TCP报头的第一个字节序号就是1001,由于1001-2000是属于一个TCP数据报的,报头里就只需要写1001就可以了。

确认序号的取值,是收到的数据的最后一个字节+1,数据(1-1000)的确认应答的确认序号就是1001,就在数据的最后一个字节1000的基础上+1。此时表示的含义就是:(1)小于1000的数据都已经确认收到了(2)主机1 应该接下来从1001这个序号开始继续发送(主机2向主机1索要1001的数据)。


确认应答小结:TCP可靠传输的能力就是主要通过确认应答机制来保证的。

  • 通过应答报文,就可以让对方知道数据是否传输成功
  • 引入序号和确认序号,准对多组数据进行详细的区分,规避先发后至引起的顺序错乱的情况。

🍥 3.2 超时重传(安全机制)

在上述确认应答的时候,只是讨论了顺利传输的情况,那如果丢包了咋办呢?丢包的情况有两种:(1)发送的数据丢了(2)返回的ack(应答报文)丢了。

发送方只是收不到ack,并不知道是那个情况丢的包。因此TCP引入了重传机制,就是在丢包的时候重新发送一次同样的数据。丢包是一个概率很小的时间所以重新发送一下这个数据报很大概率是可以成功传输的。

当前的这次传输到底是丢包了还是ack到达的慢呢?TCP又引入了时间阈值。就是说当发送方发了一个数据之后,就会等待ack,此时开始计时,如果在时间阈值之内,也还是没有收到ack,就认为是丢包了。超时重传意思就是超过了一定的时间,还没有响应,就重新传输。图文展示如下:

注意1:有一个特殊情况,由于重传,导致接收方重复受到消息。假设主机A发送的是一个支付请求,那么主机B连续收到两次,这就会引起多的消费了!!!其实TCP对于这种重复数据的传输是有特殊处理的,TCP存在一个“接收缓冲区”这样的存储空间(接收方操作系统内核的一段内存),每个TCP的socket对象,都有一个接收缓冲区,也有一个发送缓冲区。主机B收到主机A的数据,其实是主机B的网卡读取到了数据,然后把这个数据放到B的对应的socket的接收缓冲区中,后续使用getInputStream进一步使用read,从接收缓冲区来读取数据。(就可以把接收缓冲区想象成是一个阻塞队列,根据数据的序号,TCP很容易识别到当前接收缓冲分区的两条数据是否重复,如果重复,就把后来的这份数据直接丢弃掉,保证了应用程序调用read读取到的数据一定是不重复的!这里的去重操作,严格的说可以理解为一个优先级队列,或者是有序队列。因为可能会先发后至,所以TCP使用这个接收缓冲区,对收到的数据进行重新排序,使用应用程序read到的数据保证是有序的(和发送顺序一致)。)

 注意2:重传的数据是有可能再次丢包的,因此超时重传是可能重传N次的!连续重传如果都丢包了,虽然这个概率是很低的,但是如果真的出现了这个情况,只能说你的网络可能出现了重大故障。因此重传到一定次数的时候,就不会再继续重传了,就认为网络出现了故障,接下来TCP就会尝试重置连接(就是断开重连),如果重置还是失败了,那么会彻底断开连接。


超时重传小结:由于去重和重新排序机制的存在,发送发只要发现ack没有按时到达,就会重传数据,即使重复了,即使顺序乱了,接收方都会很好的处理掉这些情况。(去重和排序都依赖TCP报头的序号)


总结:可靠传输是TCP最核心的部分。TCP可靠传输就是通过 确认应答+超时重传 来体现的。

  • 确认应答描述的是传输顺利的情况。
  • 超时重传描述的是传输出现问题的情况 。

🍥 3.3 连接管理(安全机制)

TCP建立连接:

连接管理的“管理”就是描述了连接如何创建,如何断开。

👉接下来就是整个网络原理中的最高频的面试题了!

TCP三次握手(建立连接过程),四次挥手(断开连接过程)

🍥 3.3.1 建立连接(三次握手)

通信双方要各自记录对方的信息,彼此之间要相互认同。栗子:

三次握手可以简单地化为:

三次握手的作用:

(1)辅助作用:一定程度保证了TCP传输的可靠性。

(2)重要作用:验证双方各自的发送能力和接收能力是否正常。

(3)在握手的过程中,双方来协商一些重要的参数。

如何验证双方各自的发送能力和接收能力是否正常?下面介绍:

小情景:我和小白打微信电话,为了确保双方都可以听到,我们就约定一个口号:“hello hello”。


三次握手小结:所谓的三次握手,本质上是“四次交互”,通讯双方各自都要向对方发起一个“建立连接”的请求,同时各个向对方回应一个ack(应答报文),由于中间两次是可以合并的,就形成了三次握手。并且这个合并是必须的,因为 SYN和ACK 是同一时机的,是操作系统内核中完成的,应用程序感知不了也干预不了,服务器的系统内核收到 SYN 之后,就会立即发送 ACK也会立即发送 SYN,所以同一时机的必须合并,因为封装分用的时候封装分用两次一定比封装分用一次消耗的成本要高。

🍥 3.3.2 断开连接(四次挥手)

通讯双方各自发起一个断开连接,在各自给对方一个回应。

四次挥手可以化为:

注意:在断开连接中,中间的两次通信一般是不能合并的(特殊情况下可以)

FIN 的发起,不是由操作系统内核控制的,而是由服务器的应用程序,调用 socket 执行到 close 方法(或者进程退出)才会触发 FIN,ACK 则是由内核控制的,是收到 FIN 之后立即就返回 ACK 的。所以 ACK 和 FIN 之间就会相隔一个时间差,时间差的长或短是看我们代码如何写的。如果时间差很短很短,就会合并,如果很长就无法合并了。

🍥 3.3.3 TCP状态

TCP也有状态,不同的状态体现了当前TCP在干啥,状态非常复杂,这里只需要认识常见的状态就行。

(1)建立连接阶段的状态

1)LISTEN ---- 服务器的状态。表示服务器已经准备就绪,随时就可以有客户端来建立连接了,就相当于手机开机,WIFI满格,随时都可以有人找我打微信视频。

2)ESTABLISHED ---- 客户端和服务器端都有的状态。表示建立连接完成,接下来就可以正常通讯了,相当于视频打过去,对方接通了。

(2)断开连接阶段的状态

1)CLOSE_WAIT ---- 出现在被动发起断开连接的一方。表示等待关闭(等待调用 close 方法关闭 socket)。建立连接一定是客户端发起的请求,但是断开连接可能是客户端发起也可能是服务器主动发起。

2)TIME_WAIT ---- 出现在主动发起断开连接的一方。假设是客户端主动断开的,当客户端进入到 TIME_WAIT 状态的时候,相当于四次挥手完毕,此时的 TIME_WAIT 要保持当前的 TCP 连接状态不要立即释放。

为什么要立即释放而是使用  TIME_WAIT 保留一会连接呢?因为最后一个 ACK 刚刚发送,还没到达,万一这个 ACK 丢包了呢,此时 TIME_WAIT 会等待一段时间,如果等了之后也没有收到重传的 FIN,就认为最后一个 ACK没丢 ,于是就彻底释放了。如果最后一个ACK丢了,又恰好服务器重传的 FIN 也丢了,那么客户端一直等也没有等到 重传的 FIN,就认为连接可以彻底释放了。

       在三次握手和四次挥手过程中也存在超时重传,如果最后一个 ACK 丢包了,站在服务器的角度,不知道是因为 ACK丢了,还是自己发送的 SYN / FIN 丢了,所有的同一都视为 SYN / FIN丢了,统一进行重传操作。服务器重传 SYN / FIN,客户端就要回应 ACK,很明显,如果刚才彻底释放了连接,这样 ACK就无法进行,因此使用 TIME_WAIT 状态保留一段时间,就是为了处理最后一个 ACK 丢包的情况。

       那么 TIME_WAIT 具体保持多长时间,就真正的释放呢?约定一个时间:2 MSL(指的是互联网上两个节点之间,数据传输消耗的最大时间)

🍥 3.4 滑动窗口(效率机制)

确认应答、超时重传、连接管理都是 TCP 的可靠性提供的支持,既然实现了可靠性,那效率就会付出代价。可靠和效率是冲突的。所以 TCP 为了维护自己的效率也做了一些拯救措施。滑动窗口的本质就是降低了确认应答等待 ack 消耗的时间。

对于基本的应答来说,每次发送一个数据都要等待,等ack到了再发下一个 。这个模式就有点像我和你面对面聊天,你一句我一句。但这种方式的缺点是效率比较低。如果你说完一句话,我在处理其他事情,没有及时回复你,那你不是要干等着我做完其他事情后回复你,你才能说下一句话,很显然这不现实。

滑动窗口的本质就是不等待的批量发送一组数据,然后利用这份时间来等待一组数据的多个 ack,把不需要等待就能直接发送的数据的最大的量,称为“窗口大小”。上述的窗口大小就是300个字节(3个TCP段)。发送前3个段的时候,不需要等待任何ACK,直接发送;收到第一个ACK后,滑动窗口向后移动,继续发送第4个段的数据;依次类推。
滑动窗口如下图:

 本来等待 ack 的是1001-5000,接下来收到了2001的ack,说明2001之前的数据(1001-2000)都已经被确认了,此时就立即发送5001-6000的数据,就意味着等待 ack 的范围是2001-6000。滑动窗口是连续的,当批量发送了窗口大小的这些数据之后,发送方就要等待 ack 了。

如果在上述情况中出现了丢包咋办呢?丢包就有2种情况:

(1)ack 丢了

(2)数据丢失

 注意:后面补发的丢失的数据,数据的顺序并不会乱,因为TCP有个接收缓冲区,会在缓冲区里按照序号重新排队。

上述丢包重传的方式,叫做“快速重传”,重传操作只是重新传送了丢失的数据,快速重传是在超时重传机制下对滑动窗口的变形。如果在当前数据密集,按照滑动窗口的方式来传输,此时按照快速重传的方式来处理丢包;如果当前传输数据稀疏,不再按照滑动窗口方式,此时还是按照之前的超时重传处理丢包。

🍥 3.5 流量控制(安全机制)

这个机制是干预发送的窗口大小。滑动窗口的窗口越大,传输的效率就会越高(一份时间,等的ack就多),但是窗口也不能无限大的。窗口太大,会消耗大量的系统资源;发送的速度太快,接收端的缓冲区被打满,接收方处理不过来,会造成丢包,继而引起丢包重传等等一系列连锁反应。所以发送方的发送速度,不能超过接收方的处理能力。

流量控制就是:根据接收方的处理能力,协调发送方的发送效率。那么如何衡量接收方的处理能力?最简单的方法就是直接看接收方接收缓冲区的剩余大小。

🍥 3.6 拥塞控制(安全机制)

流量控制和拥塞控制共同决定发送方的窗口大小是多少。流量控制考虑的是接收方的处理能力,拥塞控制描述的是传输过程中中间节点的处理能力。

拥塞控制本质上就是通过实验的方式来逐渐找到合适的窗口大小(合适的发送速率)。

刚开始时,窗口大小为1,以很慢的速度发送数据(此处的1是一个TCP段,具体多少字节不确定),发现没有丢包,就开始扩大窗口,第1轮,窗口大小为2,扩大了一倍,依次执行下去。在初始阶段,由于窗口大小比较小,每一轮不丢包都会使窗口扩大一倍,按照指数增长,当增长速率达到阈值之后,此时的指数增长就成为了线性增长,前提是不丢包的情况下。如果在传输过程中丢包了,就把窗口大小缩小为很小的值,然后重复指数增长和线性增长的过程,

拥塞窗口不是固定的值,也是一直动态变化的,随着时间的推移,逐渐达成一个动态平衡的过程。

🍥 3.7 延时应答(效率机制)

滑动窗口的关键是让窗口的大小大一点,传输的效率就会快点,因此在接收方能够处理好的前提下,尽可能把窗口的大小放大一点。

延时应答就是收到数据之后,不是立即返回ack,而是稍后再返回。在等待的时间里,接收方的应用程序,就能够把接收缓冲区的数据消费一下,此时剩余空间就更大了。

延时应答采取的方式,就是在滑动窗口下,ack不再是每一条数据都返回了,就像上述图片所示,而是每隔一条返回一个ack。 

🍥 3.8 捎带应答(效率机制)

在延时应答的基础上,引入了捎带应答。服务器客户端程序,典型的模型就是“一问(请求)一答(响应)”。

🍥 3.9 面向字节流

面向字节流导致的一个重要问题就是:粘包问题(这里的 "包" ,是指的应用层的数据包)。由于TCP是字节流的,一次读1个字节,读N个字节都是可行的,这就导致了一次读到的数据,可能是半个应用层数据报,可能是一个应用层数据报,也可能是多个应用层数据报。接收缓冲区,把刚才收到的多个数据都放在一起,应用程序 read 读取的时候,读到哪里才算是一个完整的数据报呢?在TCP层次,没有在 socket API 中告诉我们应该读几个字节,具体怎么读,完全是看我们程序猿怎么设计的。

那么怎么避免粘包问题呢?就是:明确两个包之间的边界。

(1)约定好分隔符(2)约定每个包的长度

🍥 3.10 异常情况

1. 进程崩溃

进程崩溃了,对应的 PCB 就没有了,对应的文件描述符就释放了,相当于 socket.close(),此时内核就会继续完成四次挥手(就相当于是正常断开了)。

2. 主机关机(按照正常流程关机)

主机关机要先杀进程,然后正是关机。杀死进程的过程中,就和上面一样触发了四次挥手。

3. 主机掉电

假设接收方掉电了,发送方仍然在继续发送数据,发完数据要等待 ack,ack等不到的话就超时重传,再怎么重传都收不到 ack,如果传几次都没应答,尝试重置 TCP 连接,如果重置也失败了,那就放弃连接(单方面放弃连接)。

4. 网线断开

如果是发送方掉电了,接收方发现没有数据了,不知道是发送方不在了,还是要组织下语言稍后再发。所以接收方需要周期性的给发送方发送一个消息,确认一下对方是否还正常工作。发送的这个消息叫做“心跳包”。用心跳包来确认通信双方是否处在正常的工作状态中。这是一个保活机制。