目录
[1.TCP 协议报头格式详解](#1.TCP 协议报头格式详解)
[6 位标志位](#6 位标志位)
前言
来了来了,在年前也是把这一篇边复习边赶出来了,呼,大概有1.6万字以上,可以预见的是我们传输层TCP协议部分的内容是很多的,并且非常重要,如三次握手、四次挥手等内容都是面试常问的,在这一篇中我们会特别细致的去理解TCP的原理以及各种机制,在其中就有三次握手是什么、为什么是三次握手、四次挥手是什么、为什么是四次握手面试常考问题的解答;相信大家看完本篇会对TCP协议有更深一层的理解!!
TCP 全称为 "传输控制协议(Transmission Control Protocol"),人如其名,就是要对数据的传输进行一个详细的控制
1.TCP 协议报头格式详解

-
源/目的端口号:表示数据是从哪个进程来, 到哪个进程去
-
32 位序号/32 位确认号: 后面详细讲
-
4 位 TCP 报头长度:表示该 TCP 头部有多少个 32 位 bit(有多少个 4 字节);所以TCP 头部最大长度是 15 * 4 = 60字节【4 比特能够表示的最大十进制数为 15(二进制为 1111)。而我们约定tcp所对应报头的范围的长度基本单位是 4 个字节。所以 TCP 首部最大长度为 15×4 = 60 字节】,这个首部是包含定长标准报头和可变选项的
-
6 位标志位:
-
URG: 紧急指针是否有效
-
ACK: 确认号是否有效
-
PSH: 提示接收端应用程序立刻从 TCP 缓冲区把数据读走
-
RST: 对方要求重新建立连接;我们把携带 RST 标识的称为复位报文段
-
SYN: 请求建立连接;我们把携带 SYN 标识的称为同步报文段
-
FIN: 通知对方, 本端要关闭了, 我们称携带 FIN 标识的为结束报文段
-
-
16 位窗口大小:后面再说
-
16 位校验和:发送端填充,CRC 校验,接收端校验不通过,则认为数据有问题;此处的检验和不光包含 TCP 首部, 也包含 TCP 数据部分
-
16 位紧急指针:标识哪部分数据是紧急数据
-
40 字节头部选项:暂时忽略
4位首部长度
-
报头和有效载荷如何分离?
答:和UDP一样,TCP的标准报头也是定长的,为20个字节,再加上可变的选项片段(通常都是0),所以报头通常是20字节大小,前面我们也说了在这个标准报头中有4位首部长度字段,我们将其乘以4就得到了完整报头的长度,所以我们先不管三七二十一读前20字节,得到首部长度乘以4,然后再减去这20字节就得到了可变选项的长度;我们接着读取这个选项的长度,再往下读到的就是有效载荷了------就做到了报头和有效载荷的分离

同时我们注意到这个tcp报头中只会有完整报头长度的记录,可没有整个报文的记录,这是因为tcp是面向字节流的,是区分不清是否是一个完整的报文的,因此也表示不了报文的完整长度,所以对于tcp来说它不能也不需要设置一个叫做报文长度的字段,因为把握不住!于是就去掉报头后,把数据依次放入接收缓冲区中,让上层解析时自己拼接有效载荷、自己来区分完整的有效载荷(也就是说我们在封装之前写入有效载荷就应该在其中设置相关可以判断是否为一段完整有效载荷的字段,当然每次写入都会有一个sk_buff进行描述,报文之间是独立的,不会出现上一个报文的有效载荷和下一个报文的报头混在一起的情况)------这就是面向字节流
32位序号和确认序号
-
可靠性的本质
(这里先要说明的是我们下面乃至未来画这种通信图时,双方传递的全部都是tcp报文,最少也得是一个报头!!)

我们不对应答做应答,而是在tcp通信时双方是对等的,都可能会给对方发报文,双方都得给发送方做出应答,那么我们在客户端发送报文到服务端后,收到了应答就能保证前面给服务器发送的报文是可靠的,保证可靠性;而服务端也是如此,服务端给客户端发报文,只要能收到应答就能保证前面给客户端发送的报文是可靠的,保证可靠性;一旦站在双方角度没有收到对应应答,就认为该报文丢失,就应该重传
那么tcp一般的通信过程(暂时)如下:

这就是确认应答机制!!,只要确保报文不是最新的(也就是必须做出应答)就能保证历史报文的可靠性;但是这种方式得在发报文之前确保收到应答,太拉低效率了
于是我们就有了一个tcp更为通用的方式来传递报文

我们以客户端视角为例,可以通过先不管响应,连续发送了多条报文给服务端,那么在这之后,就可以通过计算服务端发回来的应答数是否与客户端发送数目一致来保证可靠性,但这个时候又有新的问题,那就是如果在发送报文过程中有几条报文丢了该如何确认丢失的报文、如何对应应答呢?所以为了更加细致地确保应答,我们的tcp报头中就引入了32位序号和32位确认序号
也就是在上面的基础之上加入序号和确认序号

意思就是说如果客户端只收到了401响应,这就代表前面三个报文其实服务端是收到了的,也就是在401这个报文序号之前的历史所有的信息都被收到了,只是响应没有成功发给客户端,这样可以允许一些应答丢失,因为可以甄别是哪些报文的响应的丢失进而可以重新发送(包括服务器接收时若只收到了400这个报文,就说明前面三个报文丢失了,也可确定要重传哪些报文)------保证对丢包进行重传,如果是收到了100和400,那么返回的响应确认序号只能是101,因为只有101前的报文是收到了的
这里还有一个问题,那就是接收端接收时可能会乱序,这是一个不可靠的问题,但是现在我们将报文带上了序号,也就是说我们可以通过给序号进行排序,按顺序来接收来解决乱序的问题
这个确认序号的定义是划时代的!!!
那么又有一个新的问题:为什么会有两个序号啊?一个序号就不行了吗?
答:我们会产生这个问题是因为上面是单纯从客户端一端角度去看的,所以会感觉只需要一个序号就好了,可是在实际过程中通信是双方的,服务器在返回响应的同时也可能会携带着自己报文数据------捎带应答,此时服务端发的消息既是给客户端的响应也是服务端给客户端发的报文消息(响应= 报头+数据【带给客户端的】),那么此时就得有回客户端报文响应的确认序号,也得有自己发送报文的序号;同样的客户端也是一样,所以才需要两个序号,缺一不可
我们知道发送端发送报文是发送到接收端的接收缓冲区中的,如果接收缓冲区满了,发送方不知情的情况下继续发送报文数据,接收缓冲区就会丢弃掉这些报文,虽然丢弃之后发送方没有收到对应响应之后可以超时重传,但这样做太浪费资源了,肯定是要有一个机制能够提醒发送方对方缓冲区已经收满,让其就不要再发送了
那么发送端如何尽早得知对方的接收能力呢?这就引出了我们tcp报头中的16位窗口大小字段
16位窗口大小
对方接收能力由对方接收缓冲区中的剩余空间大小决定,发送方应按量按需发送就必须得知道对方得接收缓冲区中剩余的空间大小;这个剩余空间大小的数字就由我们的16位窗口大小字段来保存
------我们这一端必须把自己的剩余接收缓冲区大小填写进我们报头的16位窗口大小处,这样把报文发送给对方时,对方就能够知道对端的剩余接收缓冲区大小
所以发送方给接收方发送报文的数量是有上限的,这个上限就取决于对方发过来的响应报文中接收缓冲区剩余空间的大小------我们把这种根据对方接收能力来动态调整我们发送速度的机制叫做流量控制(不要单纯理解为对速度的限制,而是要理解为把把速度调节到合适的程度,也可能接受能力强需要发送的速度就要快------考虑更多的是效率问题)
6 位标志位
回顾一下上面所写的对6个位标志位的大致介绍:
-
URG: 紧急指针是否有效
-
ACK: 确认号是否有效
-
PSH: 提示接收端应用程序立刻从 TCP 缓冲区把数据读走
-
RST: 对方要求重新建立连接;我们把携带 RST 标识的称为复位报文段
-
SYN: 请求建立连接;我们把携带 SYN 标识的称为同步报文段
-
FIN: 通知对方, 本端要关闭了, 我们称携带 FIN 标识的为结束报文段
tcp报文内核结构体中的标志位的体现:

也就是说我们的标志位实际在内核中就是一个比特位,未来所谓的标志位被设置其实就是将比特位设为1,不设置就是设为0
我们很容易看出分6个常用标志位是按它们使用的业务场景所划分的
但是我们可能会疑惑为什么要有标志位呢?
答:我们的接收方收到的tcp报文中一定会存在不同的报文类型,比如:申请建立连接的、发送数据的、发送响应应答的、申请断开连接的等等类型,那么我们的接收方要有应对不同类型的报文做出不同的动作,所以在我们的报文中肯定要存在能表示报文类型的字段------就是我们的标志位啦!!
弄清楚了为啥要有标志位,那么我们接下来就来分别谈谈每个标志位都各自代表什么意思:
-
SYN标志位
在聊这个标志位之前得先大致了解一下在两端发数据报文和响应之前还得先有一个建立连接的过程,这个过程叫做------三次握手
大概解释一下三次握手:第一次握手是客户端给服务端发送建立连接的请求报文,第二次握手是服务端给客户端发送确认收到并建立连接请求的报文 ,第三次则就是客户端向服务端发送确认收到的报文,而后连接建立成功就可以开始通信了(注意:前两次握手不能携带数据,只有tcp报头,因为第三次握手还没有完成,服务端还没确认客户端是否能接收到报文),在三次握手过程中,通信双方都可以在对方发来的报文中了解到对方的接收缓冲区大小,也就是说正式通信之前就可以操作流量控制了,已经可以进行双方接受能力的协商了
那么我们的SYN同步标志位就是在申请建立连接,握手过程中使用的标志位,在申请建立连接相关报文中被设置
-
ACK标志位
上面的SYN标志位是申请建立连接标志位,那么在申请建立连接的报文被发送至服务端时,服务端要给客户端做应答,此时除了要申请建立连接,还需要有能告诉客户端收到了请求的确认序号有效的标志位,那么我们的ACK标志位就来了,ACK的设置可以表明收到的是一个应答报文,即确认了确认序号是有效的(大部分情况,ack都是被常设置为1的,但是第一次握手时肯定是不被设置的)

- FIN标志位
要谈这个标志位之前我们得先了解在通信要结束时得先经历一次四次挥手的过程
大致解释一下四次挥手:
第一次挥手时客户端向服务器发送结束的请求,表示客户端不再发送数据;第二次挥手时服务器收到请求后,回复客户端一个ACK响应确认,但这个响应可能还携带有未传输完的数据(注意,在第三次挥手之前,数据还是可以从服务器传送到客户端的)第三次挥手时服务器完成数据传输后,向客户端发送一个结束请求,表示服务器也没有数据要发送了;第四次挥手时客户端收到服务器的请求后,回复服务器一个ACK响应确认。此时客户端需要经过一段时间确保服务器收到自己的应答报文后,才会进入结束状态
那么这个发送结束的请求中就需要设置FIN标志位了,即通知对方, 本端要关闭了;我们称携带 FIN 标识的为结束报文段
4.PSH标志位
客户端给服务端发报文,过一段时间后服务端的接收缓冲区满了,客户端在接收服务端应答的过程中发现它的接收缓冲区中的空间一直不够,拉低了效率,此时客户端不想等待了就在自己的报文中设置PSH标志位然后给服务端发去,此时服务端收到设置有PSH标志位的报文就得通知应用层马上从缓冲区中读走数据(当然要是我们在应用层就是不读,此时操作系统其实没办法,这不关它的事,而是我们这样做无疑是在写bug~);所以PSH标志位的作用就是提示接收端应用程序立刻从 TCP 缓冲区把数据读走
- RST标志位
我们上面的三次握手过程中,客户端给服务器最后发送设置了ack的应答时,这个报文是不会有下一个应答的,也就是服务端不会告诉客户端它是否真的收到了这个报文,那么我们的客户端是在发出这个设置了ack应答报文之后就认为自己建立连接成功了------也就是说无论是客户端还是服务端,要的是有三次的报文发送/接收的过程,就认为连接成功了,所以在第三次应答发出时客户端就认为连接成功,而服务端得等到收到这个应答时才认为连接成功,这个过程是有时间差的
好,此时就会出现一个问题,那就是要是这个第三次应答报文在发送过程中丢失了,客户端认为建立连接成功,可是服务端压根没有收到应答报文啊,自然也不会认为建立连接成功,就造成了客户端在与服务端建立连接时的连接建立是否成功认知不一致的问题
------这个时候客户端是极有可能向服务端发送报文的,此时服务端就可以给客户端发送设置有RST标志位的报文,表示要对方重新建立连接;我们把携带 RST 标识的称为复位报文段
这种问题产生的现象称为连接已重置,意思就是客户端三次握手建立连接失败了

通信过程中,连接出现任何问题,都可以进行重置
- URG标志位
URG标志位是和报头中的16位紧急指针搭配使用的,URG设置为1表示紧急指针有效,为0则无效
解释:
我们知道tcp保证可靠性用了序号来使得报文按序到达接收缓冲区,这个接收缓冲区是字节流式的接收队列,如果我们有数据要被优先读取、优先处理单纯这样是很难办到的,所以我们设置URG为1来启动16位紧急指针(当然这是特殊情况)
紧急指针是一个偏移量,标明的就是紧急数据相对其实数据的偏移量。紧急数据一般只能传递(或者说占用)一个字节,因为大多数情况下,报文都是按序到达然后被读取的,如果紧急数据太多,读取的时间太长,会破坏TCP按序到达的特性。 当然,如果是多个字节的话,具体的紧急数据范围是由使用TCP协议的应用程序或协议自行决定和定义的
这个紧急数据并不属于常规数据,这种数据叫做带外数据
2.详谈tcp机制
1.确认应答(ACK)机制

TCP 将每个字节的数据都进行了编号. 即为序列号

注意:每个报文中报头序号填写的是这些数据的最后一个字节,比如1-1000字节的数据为一个报文的有效载荷,那么报头中序号填的就是1000
每一个 ACK 都带有对应的确认序列号, 意思是告诉发送者, 我已经收到了哪些数据; 下一次你从哪里开始发
2.超时重传机制
先来理解一下丢包:

所以说没有收到应答,发送方是没有办法确认究竟是数据丢失还是应答丢失,那么只能等待特定时间,在这段特定时间中如果收不到应答,就判定报文丢失,进行重传------超时重传机制
tcp判断丢包不是客观,而是主观,收不到应答&&超时就判定为丢包了
这里有个隐藏的问题:如果真实是应答丢失了,那么发送方重传的报文就是重复的了,接收方如何操作呢?
答:接收方通过序号就能知道之前是否接收过该报文了,于是可以直接将其丢弃------去重:这也是序号的作用
那么这个特定的时间要怎么定呢?

3.连接管理机制
在正常情况下, TCP 要经过三次握手建立连接, 四次挥手断开连接

先可以看到的是我们的服务端可能要与多个客户端进行上面图中的连接,这些连接当然要被管理,还是先描述、再组织,但是建立连接肯定是有成本的------需要耗费时间和空间
接着我们需要知道connect只发起三次握手,但是并不完成三次握手,而我们的accept接收甚至不参与三次握手,只是把建立好的连接接收上去;也就是说我们的三次握手操作是由客户端和服务端的操作系统自动完成的
为什么要三次握手
其实上面我们经常提及连接时的三次握手操作,也知道了它是怎么操作的,但是我们还不知道的是为什么要三次握手
这里给出两个理由:
- 三次握手是验证通信两端全双工的最短方式,第一次握手证明了服务端可以收消息,但无法证实客户端可以发消息(因为站在客户端角度并不知道有无发送成功),第二次握手则证明了客户端可以发消息和接收消息,但是无法证明服务端可以发消息,理由和第一次握手无法证明客户端能够发消息一样,只有第三次握手了,也就是客户端给服务端发应答了,此时才能证实服务端可以发消息,三次握手之后才能刚刚好验证双方是全双工的,多一次浪费,少一次不行,就只是三次握手!! 测试全双工的本质是验证通信双方所处的网络是通畅的,能够支持全双工------外部环境允许
- 三次握手可以以最小成本100%确认双方通信的意愿,也就是双方都收到了对方的确认应答最少需要两次握手,而一开始的发起请求肯定是必须要做的操作,所以最少就是三次握手来确定双方通信意愿
(四次握手的理由也是如此,只有四次握手才能以最小成本100%确认双方断开通信的意愿)
为什么要四次挥手
首先四次挥手的过程:

也就是说四次挥手是建立双方断开连接共识的最小成本的操作
因为在大多数情况下,我们客户端是准备好断开连接了,所以发送了FIN报文,可是服务端此时可能还会有剩余数据报文没有发完,所以服务端发的报文并不能很好的捎带应答,所以服务端的发送报文步骤不能很好的合并一步,所以是一般都是四次挥手而不是三次挥手(当然如果恰巧服务端也可以立即断开连接了,那么只有三次挥手的情况也是可能出现的)
所以说无论是三次握手还是四次挥手,它们的本质其实都是四次,只不过区别在于服务端能否进行捎带应答
此时也会有一个问题,这个断开连接的过程我们知道了,可是一旦客户端给服务端发送了FIN报文,然后收到服务端的确认报文之后就立即close掉了自己的文件描述符,此时如果服务端还有剩下的报文数据没有发送过去就会出现无法发完的情况,为了面对这种问题,我们可以让此时的客户端先关闭写端,服务端先关闭读端,此时双方都变成了半双工
这种操作可以使用shutdown函数来完成局部性关闭:

(但是一般是用不到这个的,更多都是断开连接时服务端是没啥数据还剩下要发送了,就算有,在服务器端其实是会有在确保自己的缓冲区无数据了才给对方发送确认应答报文,此时大多都使用close就好)
四次挥手状态变化


一方断开连接,但是另一方没有发送fin报文,此时对方(尤其是服务端)的这条连接就一直处于close_wait的状态;如果客户端已经退出,或者关闭,服务器端就是不关闭,close_wait,此时依旧占用文件 fd,连接没有释放!

而我们知道文件描述符也是一种资源,长期占着会导致如果有大量的客户端来进行连接,文件描述符表总会有占满的那天,此时服务器就越来越卡顿甚至崩溃了,所以说文件描述符用完就必须要关掉
同时主动发起断开连接的一方收到对方的确认报文之后不能立马关闭,而是处于time_wait状态,即便是四次挥手完成了,也需要等待一段时间过后才能把自己处于close关闭状态

这里给出官方说明time_wait要等待多长时间:
TCP 协议规定,主动关闭连接的一方要处于 TIME_ WAIT 状态,等待两个MSL(maximum segment lifetime)的时间后才能回到CLOSED状态,MSL 在 RFC1122 中规定为两分钟,但是各操作系统的实现不同, 在 Centos7上默认配置的值是60s;
可以通过 cat /proc/sys/net/ipv4/tcp_fin_timeout 查看 msl 的值
那么为什么要等待的时间为2msl呢?
解释:
MSL 是 TCP 报文的最大生存时间,因此 TIME_WAIT 持续存在2MSL的话,就能保证在两个传输方向上的尚未被接收或迟到的报文段都已经消失------就是一开始发送的报文还未发到接收方,发送方和接收方就已经完成了四次挥手的情况(否则服务器立刻重启,可能会收到来自上一个进程的迟到的数据,但是这种数据很可能是错误的);
同时也是在理论上保证最后一个报文可靠到达(假设最后一个 ACK 丢失,那么服务器会再重发一个 FIN,这时虽然客户端的进程不在了,但是 TCP 连接还在,仍然可以重发LAST_ACK);
我们日常在关闭服务端然后用相同端口号再打开时,就很可能出现bind失败的错误

这种问题就是由TIME_WAIT 状态引起的!
因为我们服务器是主动关闭连接的一方,四次挥手之后处在time_wait状态,此时我们的连接是还没有彻底断开的,没有彻底释放掉连接,此时端口号还在被占用,所以此时就无法绑定相同的端口号了,就爆出了绑定bind失败的错误
(这种错误也可以逼迫服务端和客户端使用其他端口号进行连接,那么之前可能存在的尚未被接收或迟到的报文就可以被丢弃了,就不会对我们的通信产生影响)
当然time_wait也可以尽可能留有时间来确保发送方的确认报文被接收方接收到,因为2msl的时间够当确认报文接收方超时没有接收到进行重传加上发送方再一次进行发送确认报文的所需时间了,换句话说,如果发送方在2msl的time_wait时间内容没有接收到对方发来的重传,就证明四次挥手是成功的了,是个好消息
但是time_wait这种状态也是会产生一些问题的
在 server 的 TCP 连接没有完全断开之前不允许重新监听, 某些情况下可能是不合理的 :
服务器需要处理非常大量的客户端的连接(每个连接的生存时间可能很短, 但是每秒都有很大数量的客户端来请求),这个时候如果由服务器端主动关闭连接(比如某些客户端不活跃,就需要被服务器端主动清理掉),就会产生大量 TIME_WAIT 连接
由于我们的请求量很大,就可能导致 TIME_WAIT 的连接数很多,每个连接都会占用一个通信五元组(源 ip, 源端口, 目的 ip, 目的端口, 协议),其中服务器的ip和端口和协议是固定的,如果新来的客户端连接的 ip 和端口号和TIME_WAIT占用的链接重复了, 就会出现问题
所以站在应用角度:既要time_wait这种状态,又要让相同的客户端能够连接上
为了解决这一问题,我们系统提供了一个setsockopt() 方法,用来允许创建端口号相同但 IP 地址不同的多个 socket 描述符 (选项 SO_REUSEADDR 为 1 )


\^\] 照着这个模板使用 那么我们的socket代码中创建套接字时就需要加入这个方法 ```cpp void SocketOrDie() override { // 用的是系统的socket _sockfd = ::socket(AF_INET, SOCK_STREAM, 0); if (_sockfd < 0) { LOG(LogLevel::FATAL) << "socket error"; exit(SOCKET_ERR); } // 用setsocketopt设置允许创建端口号相同但 IP 地址不同的多个socket描述符 int opt = 1; setsockopt(_sockfd, SOL_SOCKET, SO_REUSEADDR, &opt, sizeof(opt)); LOG(LogLevel::INFO) << "socket success"; } ``` 四个参数参数表示的意思就是在该socket的套接字层设置地址复用,把opt取地址加上大小设置进去  此时即便主动断开连接进入time_wait状态,也能绑定相同的端口号了 但是此时也就会出现上一次尚未被接收或迟到的报文在这一次连接时影响通信的问题了,此时tcp留给我们一个杀招来降低出现这一问题的概率,那就是用报头中的序列号来判断该报文是否符合这一次连接时报文所处的初始序列号(双方在三次握手时会随机选择一个起始的序列号,比如客户端从1000开始,服务端从2000开始,双方交换序号信息),不符合的话就不发送报文,也就是使得这一问题出现的概念降至很低了 ### 4.滑动窗口机制 前面我们说过,一方发送一个报文之后等待对方的确认应答这种通信方式效率太低了  既然这样一发一收的方式性能较低, 那么我们一次发送多条数据, 就可以大大的提高性能(其实是将多个段的等待时间重叠在一起了)  那么图中的主机A一次性可以发多少报文需要有一个规定,也就是需要有无需等待确认应答而可以继续发送数据的最大值------这个最大值就是我们的滑动窗口大小,所以发送方一次向对方发送多少数据由滑动窗口的大小来决定,这个**滑动窗口就是发送缓冲区的一部分** > * 窗口大小指的是无需等待确认应答而可以继续发送数据的最大值,上图的窗口大小就是4000 个字节(四个段). > > * 发送前四个段的时候, 不需要等待任何 ACK, 直接发送 > > * 收到第一个 ACK 后, 滑动窗口向后移动, 继续发送第五个段的数据; 依次类推 > > * 操作系统内核为了维护这个滑动窗口,需要开辟发送缓冲区来记录当前还有哪些数据没有应答;只有确认应答过的数据, 才能从缓冲区删掉 > > * 窗口越大, 则网络的吞吐率就越高   滑动窗口把我们的缓冲区的数据分成了三个部分: 1. 滑动窗口左边是已发送已确认的报文,也就是这部分报文已经无效了,这部分空间可以利用了,可以不用刻意的去清空 2. 滑动窗口中是可以直接发送并且暂时不需要应答的报文 3. 滑动窗口右边是待发送的报文 序号在发送的轮次中,数字是依次增大的,也就意味着滑动窗口未来基本整体是向右滑动的(宏观) (而滑动窗口移到的本质就是让start和end"指针"向右移------下标增加) 以下是TCP中滑动窗口理解的一些关键要点: 1. **接收方窗口大小(Receiver Window Size)**:接收方在TCP报文中通过窗口大小字段告诉发送方它有多少可用的缓冲区空间。这个窗口大小是动态调整的,可以根据接收方的缓冲区情况和网络拥塞情况而变化。 2. **发送方窗口大小(Sender Window Size)**:发送方也会维护一个窗口大小,表示它可以发送多少数据而不需要等待确认。发送方的窗口大小通常受到接收方窗口大小和网络拥塞的影响。 3. **滑动窗口的操作**:发送方发送数据,并等待接收方的确认。一旦接收方成功接收并确认数据,发送方的窗口会向前滑动,允许发送更多数据。这就是为什么它被称为"滑动"窗口。 4. **流量控制和可靠性**:滑动窗口机制有助于实现流量控制,防止发送方过多地发送数据,从而导致网络拥塞。它还增强了可靠性,因为接收方可以告知发送方哪些数据已经成功接收,哪些需要重新传输 通过对上述概念的理解后,我们再来理解一下**滑动窗口的本质:发送方一次可向对方发送数据的上限** ------滑动窗口的上限主要取决于对方的接受能力(目前认知);同时滑动窗口是流量控制的具体实现方案,是除了报头中16位窗口大小的流量控制的另一部分,但是我们需要区分的是**流量控制体现更多的是数据传输的可靠性** ,**滑动窗口主要是传输数据速率的提升** 那么我们滑动窗口的start就为接收方发过来的报文的确认序号(因为确认序号之前的报文都发送过了),end就是start + 接收方此时的接收能力(也就是对方的16位窗口大小)那么当对方的16位窗口大小为0时,此时接收方的接受能力为0,此时滑动窗口大小可以为0 细节: 1. 滑动窗口不会整体向左移动 2. 滑动窗口可以变大、变小、不变和变成0,都是由对方的接收缓冲区的剩余空间大小来动态改变的,完全取决于对方的接受能力 3. 那么如果出现了丢包, 如何进行重传? 这里分两种情况讨论 情况一: 数据包已经抵达,ACK 被丢了  这种情况下,部分 ACK 丢了并不要紧,因为可以通过后续的 ACK 进行确认; 情况二: 数据包就直接丢了  > • 当某一段报文段丢失之后,发送端会一直收到 1001 这样的 ACK,就像是在提醒发送端 "我想要的是 1001" 一样 > > • 如果发送端主机连续三次收到了同样一个 "1001" 这样的应答,就会将对应的数据 1001 - 2000 重新发送 > > • 这个时候接收端收到了 1001 之后, 再次返回的 ACK 就是 7001 了,因为2001 -7000接收端其实之前就已经收到了, 被放到了接收端的接收缓冲区中 这种机制被称为 "高速重发控制"(也叫 "快重传") 我们**再来理解一下超时重传** 。超时重传**背后的含义**:就是没有收到应答的时候,数据必须被暂时保存起来,以方便后续进行重传,保存的位置就是在滑动窗口当中 这里就不得不拿快重传和超时重传进行对比了,超时重传指当发送方发送一个数据包后,会设置一个定时器,如果在超时时间内没有收到对应的确认(ACK),则认为这个数据包丢失了,触发重传 两者的区别主要集中在触发重传的条件和时机上:快重传是在发送方收到重复的确认时立即进行重传,无需等待超时时间;超时重传是等待一个合适的超时时间后,如果没有收到确认,则进行重传,如果有三次重复的确认报文,此时触发快重传,超时重传会将自己的闹钟取消(也就是取消超时重传) > 通过快重传,发送方可以快速发现丢失的数据包并立即进行重传,而不需要等待整个超时时间。这可以避免了长时间的等待,提高了传输效率。快重传的速率比超时重传快,为什么不全都用快重传呢?原因的快重传是有条件的------必须收到三个重复的确认应答的时候,如果没有收到三个重复的应答,就用不了快重传,此时就必须得使用超时重传了  需要注意的是,快重传和超时重传是TCP协议中的两种处理丢包的机制,底层支持都是滑动窗口,它们可以相互配合使用以提高数据传输的可靠性和效率 好,我们再回到细节3,上面细节3讲的始终都在于左侧报文的丢失  我们得出的结论就是最左侧报文丢失时,滑动窗口不会向右移动,必须触发重传,确认了才能向右滑动,而如果是报文应答丢失是不需要进行操作的,因为后续的确认序号已经说明了这个报文是收到的,只是丢失了应答,滑动窗口是可以继续向右移动的 > 那么如果是中间的报文丢失呢?此时后续报文的确认序号就是这个中间报文的起始序号,那么滑动窗口只需要向右滑动让start指向的是这个确认序号的位置就好了,此时就演变成了最左侧丢失的情况 > > 那么如果是最右侧的报文丢失呢?此时由于最右侧之前的确认应答都收到了,滑动窗口会不断向右移动,直到在这个原来最右侧的报文后续的报文应答的确认序号都是这个原来最右侧报文起始的序号,又演变成了最左侧报文丢失的情况啦 所以说无论是哪种情况的丢包都会演变成最左侧丢包的情况,解决办法也是一样的,都会转换成超时重传和快重传的机制;滑动窗口是不会跳过报文进行应答的,这是由确认序号的定义决定的------确认消息一定是连续确认的,发送必须连续发送 滑动窗口如果一直向右滑动,会不会存在越界问题?答案是不存在的。我们可以把滑动窗口看成一个环状结构的。具体如下图:  ### 5.流量控制机制 接收端处理数据的速度是有限的,如果发送端发的太快, 导致接收端的缓冲区被打满, 这个时候如果发送端继续发送, 就会造成丢包, 继而引起丢包重传等等一系列连锁反应 因此 TCP 支持根据接收端的处理能力, 来决定发送端的发送速度. 这个机制就叫做流量控制(Flow Control)------我们前面是有了解过的; > • 接收端将自己可以接收的缓冲区大小放入 TCP 首部中的 "窗口大小" 字段,通过ACK 端通知发送端 > > • 窗口大小字段越大, 说明网络的吞吐量越高 > > • 接收端一旦发现自己的缓冲区快满了,就会将窗口大小设置成一个更小的值通知给发送端 > > • 发送端接受到这个窗口之后, 就会减慢自己的发送速度 > > • 如果接收端缓冲区满了,就会将窗口置为 0;这时发送方不再发送数据,但是需要**定期发送一个窗口探测数据段,使接收端把窗口大小告诉发送端**------这个窗口探测数据段其实就是一个没有有效载荷的报文,发送之后不影响缓冲区大小但是接收端需要做应答,此时在应答中就有窗口大小的信息  接收端如何把窗口大小告诉发送端呢? 回忆我们的 TCP 首部中,有一个 16 位窗口字段,就是存放了窗口大小信息 > 那么问题来了:16 位数字最大表示 65535, 那么 TCP 窗口最大就是 65535 字节么(默认情况是这样的)?实际上, TCP 首部 40 字节选项中还包含了一个窗口扩大因子 M,实际窗口大小是 窗口字段的值左移 M 位 当然如果发送方长时间发送探测时接收方返回的窗口大小都是0,此时发送方会发送携带PSH标志位的报文,如果在这之后接收方的上层还是没有取出报文,那么发送方就会认定这个接收方出问题了,就会进行断开连接 ### 6.拥塞控制机制  前面我们了解到丢包时,会进行超时重传。这里的丢包是指少量的丢包。但是如果出现大量的丢包情况呢?一旦出现大量的丢包,我们就认为是网络出现了问题,一般情况下就是网络拥塞了! > 注意,服务器不只是在给你一台主句提供服务,可能会同时给大量主机提供服务。可能当前的网络状态就已经比较拥堵。在不清楚当前网络状态下,贸然重新发送大量的数据,是很有可能引起雪上加霜的;于是这里就引入了拥塞控制机制(发送方的所有主机都采用这种机制)  此时TCP 引入 慢启动机制:先发少量的数据,探探路,摸清当前的网络拥堵状态,再决定按照多大的速度传输数据: 这时会**发送少量数据,看看网络是否拥塞** 。**不只是我们自己的主机发送少量数据,是触发到网络拥塞信号的主机都会发送少量数据,这样就可以是网络在很大程度上得到缓解**  > 此处引入一个概念称为**拥塞窗口**:一个临界值,值以内,网路大概率不拥塞,值以上,网络可能拥塞;发送开始的时候, 定义拥塞窗口大小为1,每次收到一个 ACK 应答,拥塞窗口加1;每次发送数据包的时候,将拥塞窗口和接收端主机反馈的接收缓冲区窗口大小做比较,取较小的值作为实际发送的窗口 像上面这样的拥塞窗口增长速度,是指数级别的; "慢启动" 只是指初始时慢, 但是增长速度非常快 > 为了不增长的那么快,因此不能使拥塞窗口单纯的加倍,此处引入一个叫做**慢启动的阈值(**ssthresh)"当拥塞窗口超过这个阈值的时候,不再按照指数方式增长,而是按照线性方式增长"   > 当 TCP 开始启动的时候, 慢启动阈值等于窗口最大值,在每次超时重发的时候,慢启动阈值会变成原来的一半,同时拥塞窗口置回1,此时不仅是为了支持慢启动,也是重新开始探测网络健康 > > 少量的丢包, 我们仅仅是触发超时重传; 大量的丢包, 我们就认为网络拥塞 > > 当 TCP 通信开始后,网络吞吐量会逐渐上升;随着网络发生拥堵,吞吐量会立刻下降;拥塞控制,归根结底是TCP协议想尽可能快的把数据传输给对方,但是又要避免给网络造成太大压力的折中方案  慢启动不仅可以缓解网络拥塞的问题,同时还可以在中后期,网络恢复了之后,尽快恢复通信的速率 > 那么现在发送数据,不只是要考虑对方的就收能力了,还要考虑网络的情况。 **拥塞窗口本质是单台主机一次向网络中发送大量数据时,可能会引发网络拥塞的上限值**!所以现在可以给出滑动窗口大小的正确认知了------滑动窗口的大小 = min(拥塞窗口,对方窗口大小\[接受能力\] )  最后再重声明一点:网络拥塞并不是单纯的我们自己的一台主机导致的,而是很多台主机一起导致的 ### 7.延迟应答机制 如果接收数据的主机立刻返回 ACK 应答, 这时候返回的窗口可能比较小 > 假设接收端缓冲区为 1M. 一次收到了 500K 的数据;如果立刻应答, 返回的窗口就是500K;但实际上可能处理端处理的速度很快, 10ms 之内就把500K数据从缓冲区消费掉了------在这种情况下,接收端处理还远没有达到自己的极限,即使窗口再放大一些,也能处理过来 > > 如果接收端稍微等一会再应答,比如等待 200ms 再应答,那么这个时候返回的窗口大小就是1M; 一定要记得, 窗口越大, 网络吞吐量就越大, 传输效率就越高. 我们的目标是在保证网络不拥塞的情况下尽量提高传输效率 那么所有的包都可以延迟应答么? 肯定也不是 > 数量限制: 每隔 N 个包就应答一次 > > 时间限制: 超过最大延迟时间就应答一次 具体的数量和超时时间, 依操作系统不同也有差异; 一般 N 取 2, 超时时间取 200ms  总结来说,延迟应答是解决tcp的效率问题,这样就右较大概率让让发送方更新出一个更大的滑动窗口,进而提升效率 > 注意:我们上面所说的是**大概率** 。因为**发送方单次发送数据的上限是取决于网络状态(拥塞窗口)和对方的接受能力(对方的窗口)** ### 8.捎带应答机制 这个机制我们前面就提过啦 在延迟应答的基础上, 我们发现, 很多情况下, 客户端服务器在应用层也是 "一发一收"的. 意味着客户端给服务器说了 "How are you", 服务器也会给客户端回一个"Fine, thank you"; 那么这个时候 ACK 就可以搭顺风车, 和服务器回应的 "Fine, thank you" 一起回给客户端 总结来说,**捎带应答机制出现的本质也是为了提升tcp通信效率的** ### 9.面向字节流机制 这一部分我们前面也很深入的感受过啦 > 创建一个 TCP 的 socket, 同时在内核中创建一个 发送缓冲区 和一个 接收缓冲区 > > 调用 write 时, 数据会先写入发送缓冲区中,如果发送的字节数太长, 会被拆分成多个 TCP 的数据包发出,如果发送的字节数太短, 就会先在缓冲区里等待, 等到缓冲区长度差不多了,或者其他合适的时机发送出去;接收数据的时候, 数据也是从网卡驱动程序到达内核的接收缓冲区,然后应用程序可以调用 read 从接收缓冲区拿数据 > > 另一方面, TCP 的一个连接, 既有发送缓冲区, 也有接收缓冲区,那么对于这一个连接, 既可以读数据, 也可以写数据. 这个概念叫做 全双工 > 由于缓冲区的存在, TCP 程序的读和写不需要一一匹配, 例如 > > * 写 100 个字节数据时, 可以调用一次 write 写 100 个字节, 也可以调用 100次write, 每次写一个字节 > > * 读 100 个字节数据时, 也完全不需要考虑写的时候是怎么写的, 既可以一次read 100 个字节, 也可以一次 read 一个字节, 重复 100 次 由于是面向字节流的,所以会产生数据粘包问题,关于这个问题,我们在前面序列化和反序列化阶段已经讲的很明白啦,在这就不过多赘述  ## 3.Tcp总结 TCP(传输控制协议)是一种面向连接的、可靠的传输层协议,用于在网络中传输数据。以下是TCP协议中的主要机制: 1. 确认应答:TCP通过ACK回应,来告诉发送方收收到了报文数据。以避免数据在丢失的情况下,依然在传送数据。 2. 流量控制:TCP使用滑动窗口机制来控制发送方和接收方之间的数据流量,以避免发送方发送过多数据导致接收方无法处理。 3. 拥塞控制:TCP通过使用拥塞窗口和拥塞避免算法来控制网络中的拥塞情况,以避免网络过载导致的性能下降。 4. 三次握手和四次挥手:TCP在建立连接时使用三次握手,确保双方都准备好进行通信。在关闭连接时,使用四次挥手来优雅地关闭连接。 5. 超时与重传:TCP通过设置超时时间来检测丢失的数据包,并在超时后重新发送未收到确认的数据。 6. 序号和确认号:TCP使用序号来标识发送的数据段,使用确认号来确认已经接收到的数据段。 7. 滑动窗口:TCP使用滑动窗口机制来动态调整发送方发送数据的速率,以适应网络的状况。 8. MSS(最大报文段长度):MSS是TCP数据段的最大长度,通常根据网络的MTU(最大传输单元)来确定,以避免分段和重新组装的开销。 9. 延迟确认:TCP使用延迟确认机制来减少确认消息的发送次数,以提高网络效率。 10. 携带应答:在给发送方回应时,也会携带上部分有效数据,提高传输效率。 请注意,以上是TCP协议中的一些重要机制,它们共同确保了数据的可靠传输和网络的稳定性  TCP 异常情况 1. 进程终止:进程终止会释放文件描述符, 仍然可以发送 FIN. 和正常关闭没有什么区别 2. 机器重启:和进程终止的情况相同,机器掉电/网线断开: 接收端认为连接还在, 一旦接收端有写入操作, 接收端发现连接已经不在了, 就会进行 reset. 即使没有写入操作, TCP 自己也内置了一个保活定时器(就是不携带数据的报文), 会定期询问对方是否还在. 如果对方不在, 也会把连接释放,也就是说连接总是会被正常释放的,tcp对于异常连接,有很强的容错了------这就是连接的保活,是tcp自带的,时间大概是几十分钟级别的,建议应用层自行完成,为兜底策略 3. 另外, 应用层的某些协议, 也有一些这样的检测机制. 例如 HTTP 长连接中, 也会定期检测对方的状态. 例如 QQ, 在 QQ 断线之后, 也会定期尝试重新连接  基于 TCP 应用层协议 • HTTP • HTTPS • SSH • Telnet • FTP • SMTP 当然, 也包括你自己写 TCP 程序时自定义的应用层协议; **TCP/UDP 对比** 我们说了 TCP 是可靠连接, 那么是不是 TCP 一定就优于 UDP 呢? TCP 和 UDP 之间的优点和缺点, 不能简单, 绝对的进行比较 > • TCP 用于可靠传输的情况, 应用于文件传输, 重要状态更新等场景 > > • UDP 用于对高速传输和实时性要求较高的通信领域, 例如, 早期的 QQ, 视频传输等,另外 UDP 可以用于广播; 归根结底, TCP 和 UDP 都是程序员的工具, 什么时机用, 具体怎么用, 还是要根据具体的需求场景去判定 ## 4.用UDP实现可靠传输(经典面试问题) UDP(User Datagram Protocol)是一种**无连接、不可靠** 的传输协议,**通常用于快速传输数据,但不提供可靠性保证**。要实现可靠传输,通常需要再一些额外的机制,因为UDP本身并不提供这些功能。 以下是一种可能的方法,详细解释如何使用UDP实现可靠传输: 1. 确认和重传机制:在发送端,将每个UDP数据包都分配一个唯一的序列号,并设置一个超时时间。一旦发送UDP数据包,就等待接收端的确认。如果在超时时间内没有收到确认,发送端将重传该数据包。这个确认和重传机制确保数据的可靠性。 2. 流量控制:在发送端和接收端之间实现流量控制,以确保发送速率不会超过接收端的处理能力。这可以通过维护发送窗口和接收窗口来实现。发送窗口定义了可以发送的未确认数据包数量,而接收窗口定义了接收端可以接受的数据包数量。 3. 数据校验:UDP本身不提供数据校验,但你可以在应用层添加校验和,以检测数据包是否被损坏。常用的校验和算法包括CRC(循环冗余校验)和MD5等。 4. 拥塞控制:实现拥塞控制以避免网络拥塞和数据丢失。这可以通过动态调整发送速率和发送窗口来实现,以确保网络不会过载。 5. 超时处理:合理设置超时时间,以便在发生数据包丢失或延迟时及时重传数据包。 6. 缓冲区管理:在发送端和接收端维护适当的缓冲区,以处理数据包的排队和处理。 7. 错误处理:处理各种错误情况,例如重传次数超过阈值时的放弃,以及接收到重复数据包时的处理 我们知道TCP是可靠的,为了使UDP可靠,我们可以**结合TCP中的机制来实现UDP的安全可靠**! ## 5.其他底层补充 连接创建成功之后,tcp会在内核中创建一个struct tcp_sock,然后accept的时候,操作系统就为我们创建一个套接字对象struct socket,创建一个struct file,把这两指针互相指向,然后使用struct socket中sk指针指向下一个tcp,把file填入到文件描述符给上层返回,然后就有了两套套接字  如果是udp也是一样的,只不过是没有了网络连接的套接字结构体  ## 尾声 ok,我们的传输层的UDP和TCP协议也就讲到这里啦,还是有不少难度在的,那么接下来就继续往下层走,就到我们的网络层啦!
