个人主页:小则又沐风
个人专栏:
• [数据结构](https://blog.csdn.net/jiaomorning/category_13115833.html?fromshare=blogcolumn&sharetype=blogcolumn&sharerId=13115833&sharerefer=PC&sharesource=jiaomorning&sharefrom=from_link "数据结构")
• [竞赛专栏](https://blog.csdn.net/jiaomorning/category_13117056.html?fromshare=blogcolumn&sharetype=blogcolumn&sharerId=13117056&sharerefer=PC&sharesource=jiaomorning&sharefrom=from_link "竞赛专栏")
• [C语言](https://blog.csdn.net/jiaomorning/category_13115832.html?fromshare=blogcolumn&sharetype=blogcolumn&sharerId=13115832&sharerefer=PC&sharesource=jiaomorning&sharefrom=from_linkhttps://blog.csdn.net/jiaomorning/category_13115832.html?fromshare=blogcolumn&sharetype=blogcolumn&sharerId=13115832&sharerefer=PC&sharesource=jiaomorning&sharefrom=from_link "C语言")
• [C++](https://blog.csdn.net/jiaomorning/category_13141438.html "C++")
• [Linux](https://blog.csdn.net/jiaomorning/category_13157216.html "Linux")
• [OJ项目](https://blog.csdn.net/jiaomorning/category_13197107.html "OJ项目")
• [MySQL](https://blog.csdn.net/jiaomorning/category_13202545.html "MySQL")•[GIT](https://blog.csdn.net/jiaomorning/category_13205841.html "GIT")
前言
回顾:
在上一篇的文章中,我们认识到了TCP协议的结构.
总的来说,TCP协议通过源端口号和目的端口号分别来标识自己的出发点和目的地,并且通过序号来表明自己这次发送的数据也通过确认序号来交给对方自己收到的数据.
由于TCP协议的报头的长度不是一个固定的值,所以依靠一个名为4位首部长度的字段来记录报头的长度,实现数据和报头的分隔.
了解到TCP的可靠性是通过序号和确认应答,超时重传来保证的.
今天我们来继续学习TCP协议的知识.
TCP协议的工作模式
串行工作模式
什么是串行的呢?
简单的来说:
串行就是一条一条的发送
对方发送一个信息,收到的一方就是需要ACK应答.
这样的工作方式有什么优点呢?
首先串行的工作方式,我们发送的数据的顺序一定是有序的.
但是缺点就有一点的明显了.
我们发送每一条信息都需要等待对方的ACK应答,换句话来说就是我们如果没有收到对方的确认应答的话,我们就会等待甚至是重新发送.
这样的结果就会导致一个问题就是效率低下.
所以我们一般的工作模式一般都不会选择这个串行的.
并行工作模式
总结上述工作模式的缺点,并行工作模式就是发送方将会发送一串的数据(例如:直接发送5条)
对方对自己收到的信息进行应答就可以了
这样我们大大的提高的信息的吞吐量.明显的提高了通信的效率.
但是这样的话,就会出现一个问题:我们一次性的发送了这么多的数据,我们怎么才能知道哪个数据成功的被接受的呢?哪个数据在传输的时候丢失了呢?
超级简单的.我们协议中的报头中不是有序号和确认序号吗?
这样ACK的报文就可以来反映出自己收到的数据了.
确认序号是这样规定的:
确认序号代表的是确认序号之前的数据都是收到的.请在确认序号开始发送
那么这就很明显了.
假设我们发送的数据是1000-1200 1200-1400 1400-1600
中间的1200-1400丢失,那么ACK的确认的序号就是1200.
但是这时候会产生一个疑问了:
这样的话为什么还有设计出两个序号呢?不能只用一个确认序号吗?那个序号有什么用啊不是代表发送数据区间的信息吗?
是这样的,我们的确发送数据需要在确认序号开始发送,似乎我们的序号好像可以用确认序号+1直接获得,那么序号设置的意义在哪里?
首先有一个原因就是我在数据交互的时候,难道是只有一方在输出,另一方只是听,点头吗?
不是的,双方都有需要发送的数据,所以这个序号需要记录自己向对方发送的数据啊.
不仅仅如此更重要的原因是这样的:
我们在做应答的时候我们并不是单纯的ACK而是一个捎带应答.
什么是捎带应答?
简单的来说就是,在做确认应答的时候我们把我们需要发送的数据一起发送出去.
这时候就需要把我们的发送给对方数据的序号和确认序号分开了.
滑动窗口和流量控制
滑动窗口
在通常的情况下,我们使用的就是并行的工作模式了,但是具体是怎么一次性的发送数据的呢?
这时候我们就需要引入滑动窗口了.

在上图中的窗口大小是4000字节的.
窗口大小的实际意义是什么?
窗口大小就是我们无需等待应答可以一次性直接发送的数据.
那么现在我们将需要发送的数据分为三个部分:
- 滑动窗口的左边:这些的数据都是发送过并且确认应答的数据(已经没用了)
- 滑动窗口中:这些数据是无需等待应答直接发送的数据
- 滑动窗口右边:这些数据是还未发送的数据
那么数据在发送的时候我们只需要对滑动窗口进行移动就可以了.
那么我们在传输数据的时候数据丢失会有三种情况:
- 最左的丢失
- 中间的丢失
- 最右边的丢失
最左边丢失
最左边丢失的话,根据我们之前的学习的话,确认应答的序号就是最左边的序号,在滑动窗口上的体现就是滑动窗口没有改变.
中间丢失
中间丢失的话,代表我们的丢失数据的左边的部分是没有丢失的,所以我们的窗口就会进行滑动.
直至丢失的部分成为窗口的最左边.
这样我们的问题就成为了第一个类型的问题了.
最右边丢失
相同的道理,我们可以将窗口滑动即将丢失的部分作为我们的最左边.
这样我们就可以解决问题了.
流量控制
为什么需要进行流量控制?
解释这个问题,我们需要知道的前提就是对方的接收缓存区的大小是有限的.
假设对方的接收缓存区只剩下3000字节的空间了.但是我们设置的窗口的大小是一个4000字节的话,对方的接受能力不足以接收到我们的数据了.
所以我们需要按照对方的接收能力来控制我们的窗口大小(当然窗口的大小不仅仅是被这个因素影响,还有网速影响之后我们会进行讲解的).
那么我们是怎么把我们的接受能力传递给对方的呢?
在我们TCP协议的报头中,有一个字段就是一个窗口大小.这个会将我们的接受的能力传送给对方.
拥塞控制
在我们日常上网的时候,我们都遇到过的情况就是网络太卡顿了,这样我们上网的质量就会下降。
首先我们知道的是每个人的报文都是网络上传输的,当上网的人数多的时候,报文也会跟着变多,这样我们成功抵达对应的主机的时间就会变长。
为了避免在网络上的报文太多,造成拥堵,TCP设计了进行拥塞控制的算法。
当我们刚进行连接的时候,我们发送的报文数量是很少的(之前讲我们发送的数据数量是和对方的接收缓存区有关的,但是那是不准确的还是受网络情况的影响)具体的行为是这样的。
- 慢启动阶段:cwnd 指数增长,直到达到 ssthresh
- 拥塞避免阶段:cwnd 线性增长,直到发生拥塞
- 拥塞处理阶段:ssthresh 减半,cwnd 重置为 1,重新进入慢启动
所以我们发送数据窗口的大小是min(cwnd(拥塞窗口),rwnd(滑动窗口))。
TCP 零窗口探测
当对方的接受能力为0的时候,也就是说对方的接收缓存区的可用的大小为0的话,对方不再要求我们给他发数据,但是这样的话就会出现一个问题了.我们怎么知道他将缓存区中的数据提取走了呢?
为了进行探测对方缓存区的可用空间.
我们每隔一段时间就会向对方发送一个携带一个字节有效数据的报文
为什么是一个字节的报文
这就是利用了TCP协议的应答机制,当我们给对方发送有效数据的时候,对方就必须给我们做出应答.
再发送ACK的时候,对方的报头就会携带着窗口的大小.
标志位字段详解
这些都标志位底层就是类似一个位图一般.
ACK 标志(确认标志)
ACK标识的就是确认序号的有效性.
也就是说当我们收到ACK的时候就代表对方真正的收到了.
因为我们在真正通信的时候,都是进行捎带应答的,所以我们的报文ACK的标志位都是置为1的.
SYN 标志(同步标志)
这个标志位就是用来建立连接的,是用于建立连接的三次握手时候使用的.
FIN 标志(结束标志)
这个标志位就是用来结束通信的标志位.
使用的场景就是我们结束通信时候的四次挥手的.
其他三个标志位的作用:
- URG:紧急指针有效,表示报文中有紧急数据
- PSH:提示接收端立即将数据从 TCP 缓冲区推送给应用层
- RST:强制重置连接,用于处理异常情况
TCP 连接管理机制
三次握手
我们之前学习的知识,都是建立在我们的连接已经建立起来的,但是我们并不知道来凝结是怎么建立起来的.
我们来看这个故事.
有一对男女他们甜蜜蜜的暧昧了两个月之久,在这一段的时间里,他两个的关系就是差捅破那张窗户纸了.但是他们都不说而已.
有一个寂静的夜晚,他们行走在宁静的街道上,手牵着手低着头,似乎彼此之间只剩下了对方的心跳的声音.
峰回路转,来到了一片草地,四下无人,男生看着对方犹如蜜桃一般一掐就能出水的脸庞,心跳似乎停滞,男生不想只是远远的伫立欣赏,他想永久的封存这美好的一切.
男生抬起头抿嘴:
你可以做我的女朋友吗?
女生似乎早有料到但是还是有所停滞,似乎又出乎意外,他的眼中闪着星光,俏皮的问:
可以啊,什么时候?
男生不想再等待,只想将一切美好的事物拥入怀中,紧紧的抱住对方,说:
就现在!
此后他们开始没羞没臊的恋爱了.
ok这就是经典的TCP建立连接的流程.
首先男生发起了连接的请求.对方接收到了连接的请求,并且答应了他的请求并且询问什么时候建立连接,男生得到了对方接收了连接的请求回答了建立连接的时间.
这就类似TCP的三次握手
那么这时候我们就会有一个问题就是:为什么采用的是三次握手的呢?不能四次或者是两次吗?
首先我们需要知道的是三次握手是最高效建立连接的方式了.
客户端向服务器发起了连接的请求->服务器收到了请求接收了请求并且向客户端发起了连接的请求->客户端收到请求,做出回答.
但是我们可以仔细的想想这三次握手的实质是什么.
- 第一次握手:
服务器知道客户端可以成功的发送信息.
- 第二次握手:
客户端知道服务器能够接收信息和发送信息.
- 第三次握手
服务器知道客户端可以接收信息
所以我们知道了 这样的设计就是为了在最小握手次数下确认客户端和服务器的信息接收和发送的能力。
四次挥手
上面我们知道了客户端和服务器简历起连接是依靠于三次握手的,但是我们并不知道他们是如何进行关闭连接的。
我们还是刚才的例子:
张三和李四甜蜜的在一起了,但是他们之间的相处并不是那么的融洽,有一天爆发了争吵,
张三是在是忍受不了了,对李四说:
我们分手吧。
李四收到这个断开连接的信息之后,说:
好。
李四过了一会也对张三说:
我们分手吧。
张三说:
好。
从此二人再无瓜葛。
这时候我们发现的是断开连接的时候并不是进行了三次的交互,而是进行了四次的交互。
我们称每一次的交互为一次的挥手。
但是这里的断开连接为什么是四次挥手的呢?
我们现在就来了解一下吧。
首先客户端想要断开连接就需要发送携带FIN标志位的信息。
服务端收到这个信息后他就知道了对方要和他断开连接了,但是为什么服务端也直接发送断开连接的信息呢,这是因为可能服务端向客户端发送的数据还没有全部发送完成。
所以服务端只能发送一个接收到断开连接的信息。
当服务端的信息全部发送完成之后,服务端就会发送断开连接的请求了,这时候客户端就需要收到信息做出一个应答。
综合上面的情况就是四次挥手了。
异常断开连接
在我们日常使用电脑的时候,我们遇到过下面的情况:
正在看抖音的时候我们的网络断开了,这时候我们的客户端就会和断开了连接。(网络只是断开一会)
这时候是这样的情况:
"体面"的断开连接
什么是体面的断开来连接呢?
我们常见的场景是:我们自己杀死了一个进程,电脑正常进行关机。
这时候内核(操作系统)就会帮你进行文件描述符的释放,换句话来说就是:内核会帮你完成四次挥手的行为。
"非体面"的断开连接
例如我们网断了,电脑直接断电了。
服务器并不知道客户端断开了连接,正常处理来自客户端的数据,但是在传给客户端的时候,发生了一个问题:客户端会感到疑惑,我没有和这个服务器建立连接他为什么要给我发送数据?可能是我断开连接了,因为这时候我们的服务器迟迟收不到ACK他就是不停的超时重传,内核因为找不到对应的socket就会发送一个报文携带RST,代表要服务器断开旧连接。
TCP的保活机制
如果服务器长时间不给客户端发送数据的话,服务器怎么才知道客户端没有拔网线跑路了呢?
这是因为服务器会定时发送一个探测报文,意思就是"小兄弟,你还在嘛?"来保持连接。
