这里写目录标题
- [<font color="FF00FF"> 1. UDP](# 1. UDP)
-
- [<font color="FF00FF"> 1. UDP的理解](# 1. UDP的理解)
- [<font color="FF00FF">2. UDP协议端格式](#2. UDP协议端格式)
- [<font color="FF00FF">3. UDP的缓冲区](#3. UDP的缓冲区)
- [<font color="FF00FF">4. 报文的理解](#4. 报文的理解)
- [<font color="FF00FF">2. TCP](#2. TCP)
-
- [<font color="FF00FF">1. TCP协议段格式](#1. TCP协议段格式)
- [<font color="FF00FF">2. TCP的可靠性](#2. TCP的可靠性)
- [<font color="FF00FF">3. 流量控制](#3. 流量控制)
- [<font color="FF00FF">4. 标志位](#4. 标志位)
- [<font color="FF00FF">5. 序号的理解](#5. 序号的理解)
- [<font color="FF00FF">6. 如何理解丢包和超时重传](#6. 如何理解丢包和超时重传)
- [<font color="FF00FF">7. 连接管理机制](#7. 连接管理机制)
-
- [<font color="FF00FF">7.1 TCP三次握手](#7.1 TCP三次握手)
- [<font color="FF00FF">7.2 TCP四次挥手](#7.2 TCP四次挥手)
- [<font color="FF00FF">7.3 如何解决bind失败问题](#7.3 如何解决bind失败问题)
- [<font color="FF00FF">7.4 整个TCP建立连接和关闭连接的状态变化](#7.4 整个TCP建立连接和关闭连接的状态变化)
-
- [<font color="FF00FF">1. 服务端](#1. 服务端)
- [<font color="FF00FF">2. 客户端](#2. 客户端)
- [<font color="FF00FF">8. 滑动窗口](#8. 滑动窗口)
-
- [<font color="FF00FF">1. 快重传](#1. 快重传)
- [<font color="FF00FF">9. 拥塞控制](#9. 拥塞控制)
- [<font color="FF00FF">10. 延时应答](#10. 延时应答)
- [<font color="FF00FF">11. 粘包问题](#11. 粘包问题)
- [<font color="FF00FF">12. TCP异常情况](#12. TCP异常情况)
- [<font color="FF00FF">13. 用UDP实现可靠传输(经典面试题)](#13. 用UDP实现可靠传输(经典面试题))
1. UDP
1. UDP的理解


- UDP是传输层协议,向上分用时用目的端口号确认,交给哪个进程,因为进程在网络通信时会绑定端口号,网络通信就是两个进程通信,源ip和目标ip是向下封装时添加的报头,需要确定主机,而这个TCP6是协议号,网络层ip向上分用时,需要确认把有效载荷交给哪个具体的协议
2. UDP协议端格式

-
UDP怎么做到报头和有效载荷分离?
UDP的报头是固定的,8字节,整个报文大小也是确定的,16个比特位表示UDP的整个报文大小,如果有有效载荷,那用16位-8字节报头,有效载荷就确定了 -
UDP如何分用?
UDP向上分用时,去掉自己的报头,用16位目的端口号,把有效载荷交给指定的进程,分用完成
端口号为什么是16位的呢?
因为内核协议的端口号是16位的
UDP为什么叫面向数据报?
因为它报文和报文直接是有边界的,因为OS知道每个UDP的报文和报头的大小,OS能计算出一个UDP的有效载荷的大小,所以向上层交付数据时,它能做到上传一个UDP完整的有效载荷,如果是多个也能分区每一个,不会出现给对端主机缓冲区发送报文时,只发了一个半UDP有效载荷的情况,它是完整的,并且发和读的次数对应
协议是结构体,这是内核里udp的结构体,OS系统都是C语言写的,所以OS之间直接传递结构体对象,向下封装报头就是结构体的添加

3. UDP的缓冲区
UDP没有真正意义上的发送缓冲区,调用sendto会直接交给内核,由内核将数据传给网络层协议进
行后续的传输动作
UDP具有接收缓冲区,但是这个接收缓冲区不能保证收到的UDP报的顺序和发送UDP报的顺序⼀
致,如果缓冲区满了,再到达的UDP数据就会被丢弃
UDP的socket既能读,也能写,这个概念叫做全双工
发送缓冲区存在的意义是如果发送的数据丢失了,需要再次从发送缓冲区里把数据拷贝到网卡再到网络,所以当对方没有成功拿到完整信息之前,缓冲区内的数据是不能清空的,而UDP不需要保证数据的可靠性,所以无需发送缓冲区,直接发送数据到内核,然后操作系统直接发送到网络,而TCP有发送缓冲区是因为TCP要保证可靠性
4. 报文的理解
如果应用层正在进行报文解析处理,会不会影响OS从网络读取报文?
不会,因为OS本身就是受外部设备的中断来执行工作的,时钟中断让操作系统在调度进程,就是在cpu上处理报文,网卡也会触发中断,此时操作系统会继续响应中断从网络读取报文,如果是单CPU可以并发的执行,多cpu可以并行处理,最后这两个工作都会被处理完成
在OS内部,每一层都可能会同时存在大量的报文,因为客户端:服务器 = n:1,而这些报文OS要以先描述再组织的方式管理起来

这个图是内核里报文的结构体,也就是先描述,再组织可以看到有next和prev指针,
可以把一个个的struct sk_buff组织起来
最下面的char*指针是管理内核缓冲区的,这个报文要有自己的内核文件缓冲区,保存报文的相关信息,就是虚拟地址到物理地址的映射,在物理内存开辟一块空间,然后把报头和有效载荷发进去,我们把这个结构体+这块内存空间的报头+有效载荷叫报文,跟进程类似,就是PCB+自己的报头和有效载荷叫报文

head指向这块缓冲区的开始,tail指向结束,尾空间不考虑,本质就是指向虚拟地址空间数据区内的这部分的开始和结束,然后data一开始指向应用层数据的起始,向下封装就是让data指针的指向向上移动,而分用就是让data指针的指向向下移动,就是+ - 对应层的协议长度
所谓的封装和解包就是移动data指针在缓冲区内部的指向
怎么移动?
-
- 对应层的协议长度

让data减去报头长度后,data移动对应的协议报头大小,data指向新的空间,之后指针强转,就可以对对应报头的空间添加数据,拿指针添加 data->,所以封装添加的都是结构体空间
- 对应层的协议长度
2. TCP
1. TCP协议段格式

TCP协议如何交付?
目的端口号分用时交给指定的进程,跟UDP一样
TCP的报头不算选项的情况下,至少是20字节的,这里的4位首部长度表示的是整个报头的大小,包含选项,如果基本单位按照比特位来算,四位数字的表示范围是[0,15],很显然不对
我们约定基本单位是四字节,所以tcp的报头范围就是[0,60],但是TCP的报头至少是20字节,所以四位数字的表示范围是[5,15],那TCP的报头范围就是[20,60],对这个四位数字的结果*4就是tcp的报头长度
TCP如何做到报头和有效载荷分离?
可以读取报头中的4位首部长度属性,*4就是报头长度,然后就能分离了
这里怎么没有报文大小,只有报头大小呢?
因为它是面向字节流的,它如果有报文大小就是面向数据报了,OS不清楚TCP的有效载荷大小,所以才会出现发送的时候发1个半,读取的时候读不到完整报文的情况,需要用户自己定制协议,拿到一个完整的有效载荷,这就是为什么TCP面向字节流,而TCP有发送缓冲区是因为TCP要保证可靠性
2. TCP的可靠性

通信的时候,双方传递的全部都是tcp报文,最少也得是一个报头
- TCP能保证发送数据是100%可靠的吗?
比如两个人在一个山上,相距200m,李四对张三大喊说什么时候下山吃饭?但是距离太远了,他不能保证对方是否能听到
只有张三回答说12点吧,李四才能保证张三一定收到了李四说的话,也就是说如果下次李四再继续对张三大喊,必须收到张三的应答,才能保证李四这次发送的消息是可靠的,否则不能保证,对应图中如果没有红线应答,那黑线就不能保证可靠性
- 收到应答,可以保证对历史消息的有效性,并且是100%保证
这个历史消息就是收到应答的上一个黑线(黑线是请求,红线是应答)
通信中,最新的报文永远没有应答,最新请求的可靠性无法保证
新的请求不能保证,除非有应答(黑线是请求,红线是应答)
让报文不是最新的就可以100%保证传递tcp报文的可靠性
保证TCP报文的可靠性,处于核心地位的是确认应答机制,上述只是方便理解,但其实不是那样的

其实当客户端向服务端发送tcp报文时,或者服务端向客户端发送tcp报文时,服务器或客户端的OS自动对对端进行应答,但是不对应答做应答,否则就循环了
如果没有应答,证明对端发送的tcp报文不完整,重新发送,如果应答了证明发送的报文一定是完整,100%可靠的,这是tcp一般的通信过程
但是这样效率太低了,发送一个报文,应答一次,应该一次发多个,应答也一样

这是TCP最通用的过程,我这里写的是客户端到服务端,反过来一样
如果没有这里的第四次应答(红色)呢?
那客户端只能收到三个报文,那客户端怎么知道是哪个报文丢失了呢?
所以在TCP报头中存在32位序号和32位确定序号,确定序号 = 序号+1
如果401的这个确定序号的报文客户端没有接收到,此时客户端的OS检查报头里的序号,发现其它的接收的应答报文的确定序号都有,就没有401,此时重新发送
上面说过收到应答,可以保证对历史消息的有效性,并且是100%保证
对应到这里是401确定序号之前的报文(100~400)已全部收到,所以如果是301没有收到,OS系统并不会重新发送300报文,允许少量报文丢失,为了提高效率,所以这是TCP最通用的过程,这个方式不一定能保证TCP的100%可靠性
下一次发送报文,从确定序号开始发送就是从401之后随便一个数字开始发送报文,因为要保证之前报文的可靠性
接收乱序问题,就是先发的报文,不一定先应答,这个问题是不可靠问题的一种
为什么要有序号和确定序号呢?
- 指定报文序号之前的所有信息,已全部收到,保证报文可靠性
- 利用序号排序,解决接受乱序问题
这里的收到指的是只要一端发的报文对方收到了就算收到,即便应答丢失,也算收到

服务器对客户端应答的时候,不光是发送应达报头,还要有自己的有效载荷,我们把它叫做捎带应答,如果是服务端和客户端发报文,客户端也要应答,也是报头+有效载荷
为什么这里要用两个序号呢?一个序号不行吗?
因为服务器既需要对多方的报文做确认,自己的报文也要有序号
就是说服务器应答客户端发送的报文里带有确定序号,来保证客户端的报文的可靠性和解决乱序问题,但是自己也要有序号来保证自己应答的报文的按序到达,还有可靠性,所以要两个序号,如果不可靠重新应答
3. 流量控制
TCP报头里还有一个属性是16位窗口大小,它表示的是自己接收缓冲区的剩余空间大小,就是个数字
为什么要有这个属性呢?
因为如果没有,发送端不知道对端的接收能力,会向对端一直发送报文,就算对端的接收缓冲区满了,它还有一直发,但此时对端会把报文丢弃,tcp虽然可以重传,但是浪费,低效,因为对端的接收缓冲区满了,你就不该继续发送报文了
所以就有了这个属性,就可以知道对方的缓冲区大小,进而按需按量发送,就可以解决浪费和低效问题,发送的数据要有上限,我们把它叫做流量控制,主要解决效率问题
可能一开始的时候客户端就向服务端发送了很多报文,但此时客户端并不清楚服务端的接收缓冲区的剩余空间大小,那怎么保证按需发送呢?
TCP在真正的发送数据前,会进行三次握手,三次握手的前两次互相发送的都是报头,就能够确定客户端和服务端双方的接收能力了
4. 标志位
tcp也是协议,内核有结构体描述它,这里的标志位就是比特位


为什么要有标志位?
客户端可能有10000个,每个客户端向服务器发送的报文类型都可能不同
比如:申请连接的报文,正常发送数据的报文,断开连接的报文等等
所以接收方收到的tcp报文,一定会存在不同类型,针对不同类型的报文,接收方要有不同的做法
标志位的存在是让对端拿到我发送的报文,明确知道要怎么处理这个报文

-
ACK标志位:确认号是否有效,表明报文是一个应答报文
ACK标志位几乎被长设为1,因为有大量的应答,或应答+数据 -
FIN:通知对方,本端要关闭了,我们称携带FIN标识的为结束报文段
-
PSH:提示接收端应用程序立刻从TCP缓冲区把数据读走
比如客户端一直向服务器发送报头请求写入数据,而服务器一直应答客户端报头是缓冲区满了,就是16位窗口大小为0,此时客户端把psh置为1,告诉服务端让OS赶紧把数据交付到上层,此时就可以继续写了,其实是不让read阻塞了 -
SYN:请求建立连接,我们把携带SYN标识的称为同步报文段
-
RST:对方要求重新建立连接,我们把携带RST标识的称为复位报文段

客户端向服务器发送SYN,请求向服务端建立连接,服务端给客户端发送捎带应答,表示我收到了你的连接,我也要向你发送连接请求,然后客户端向服务器发送ACK,表示服务器连接成功,但是客户端只要把这个应答发送出去了,就认为双方连接成功了,但是服务器可能没有收到客户端的应答,认为双方连接并没有成功,此时导致双方连接是否成功的认知不一致,然后客户端接下来会继续向服务器发送数据,但此时服务器认为连接不成功,客户端不应该向我发送数据,此时应答对方报头,并把RST置为1,表示对方重新向我建立连接,而发送数据到对端是要时间的,所以线是斜着画的 -
URG:紧急指针是否有效

报文是有序号的,来保证按序到达的,但如果我们有数据要休闲处理呢?
此时URG表示紧急指针生效,表示当前报文的有效载荷中,特定偏移量处,有紧急数据,这个紧急数据只有一个字节,就算状态码,0,1,2表示不同含义,可以有效处理,就是在接收缓冲区插对
recv:标记位设置成MSG_OOB,表示接受的数据是紧急数据,紧急数据也叫带外数据
5. 序号的理解
TCP将每个字节的数据都进行了编号,即为序列号

可以把发送缓冲区看成一个char类型的一维数组,然后用户把数据发送到缓冲区里,每个字节不就天然有编号了
但是一般以数据的最后一个下标作为序号,比如1000,然后确认序号返回1001,表示1001之前的数据全部收到
每一个ACK都带有对应的确认序列号,意思是告诉发送者,我已经收到了哪些数据,下⼀次你从哪里开始发,但是根据报文大小,我下一次发送,不应该从1001开始发送,而是,下次再发送数据应该从2000序号发送,返回2001,表示2001之前的数据全部收到了,因为报文大小假设是1000字节
6. 如何理解丢包和超时重传


发送端没有收到应答,意味着丢包吗?
只能意味着发送方的数据可能丢失,或者对方应答时丢失,要么数据丢,要么应答丢,但是无法确定具体是哪一个丢了
所以需要特定的时间等待应答,如果在特定的时间间隔,收不到应答,判断报文丢失,所以收不到应答&&超时才叫丢包,此时重新发送数据,这个过程叫超时重传,丢包是我们规定的
按照图二,如果重新发送数据,会导致数据重复问题,但是无所谓,因为有序号,可以去重,相同序号报文直接舍弃
超时的时间(特定的时间间隔)如何确定?
这个时间的长短,随着网络环境的不同,是有差异的
如果超时时间设的太长,网络也很差,会影响整体的重传效率
如果超时时间设的太短,但是网络很好,有可能会频繁发送重复的包
TCP为了保证无论在任何环境下都能比较高性能的通信,因此会动态计算这个最大超时时间
Linux中(BSD Unix和Windows),超时以500ms为⼀个单位进行控制,每次判定超时重发
的超时时间都是500ms的整数倍
如果重发⼀次之后,仍然得不到应答,等待2500ms后再进行重传
如果仍然得不到应答,等待4 500ms进行重传,依次类推,以指数形式递增
累计到一定的重传次数,TCP认为网络或者对端主机出现异常,强制关闭连接.
7. 连接管理机制
7.1 TCP三次握手
状态就是整数,就是宏,一个服务器可能会同时存在大量的连接,所以OS要对连接进行先描述,再组织的管理起来,创建struct link结构体,填写字段,建立连接有成本,消耗时间空间

connect发起三次握手,由client和server的OS自主完成三次握手的过程,accept不参与三次握手的过程
TCP真的是三次握手吗?
其实TCP本质是四次握手,只是说把ACK+SYN合并了,因为客户端对服务器的请求服务器要无脑接收,也就要先应答,然后再请求,那干脆合并在一起,变成捎带应答不就好了,然后客户端再继续向服务器应答,前两次握手就100%能完成,但是不保证第三次握手的可靠性,所以握手可能失败

为什么TCP要进行三次握手呢?(很重要)
以最短的方式,验证全双工,本质是验证:双方所处的网络是否流畅,能够支持全双工
以最小的成本,100%确定双方的通信意愿
tcp的全双工是能读能写的,这三次握手可以确定服务器和客户端双方都可以互相接收消息和发送消息
如果只有一次或两次握手,无法确定双方是否完成通信,而三次握手能100%确定双方的通信意愿
7.2 TCP四次挥手

为什么这里的ACK+FIN一般不合并呢?怎么不叫不命名为TCP三次挥手呢?
首先客户端向服务器断开连接,然后服务器给客户端应答,表示客户端对服务器的连接断开了,但是服务器和客户端还没有断,它可以继续向客户端发消息,然后把消息发完了再断开服务器对客户端的连接,然后客户端给服务器应答,表示双方都互相断开连接了,所以一般不合并,但是也有情况会合并,就是服务器没有数据发了,应答后立刻断开连接,此时就能合并,捎带应答是可以被应答的,本质是对服务器数据的应答
断开连接的本质:建立双方断开连接的共识
C->S:我要给你发送的数据已经发送完了,我要和你断开
S->C:我要给你发送的数据已经发送完了,我要和你断开
双方都要和对方断开,close就是完成关闭连接和应答的过程,所以两次close就能保证双方断开连接
这里就算客户端先关闭文件描述符,服务器的OS也会把缓冲区里的数据刷过去的,不用考虑文件描述符被关闭的问题

一般2号手册里的函数都是系统调用,这个系统调用可以只关闭读,或者写,或者读写,也可以用这个系统调用来关闭文件描述符,但是没必要
文件描述符用完必须关掉,否则fd泄漏问题
如果c已经退出,或者关闭,服务器端就是不关闭,此时服务器的状态是close_wait,等待关闭状态,依旧占用fd,连接没有释放,客户端是很多的,如果一直连接,而服务器一直不关闭,此时服务器就可能会把OS的空间用完,OS直接杀掉服务器,这个服务器就挂了
图里的客户端只要给服务器应答它就认为四次握手完成了,而服务器只有收到应答才认为四次握手完成,所以这里客户端会先断开
主动断开连接的一方,要进入一个状态,叫TIME_WAIT,就算四次握手完成,所以这里的客户端会进入TIME_WAIT状态
主动断开连接的一方,等待两个MSL的时间后才能close
MSL 是 TCP 报文的最大生存时间
为什么要 TIME_WAIT?并且时间还是 2MSL?
-
TIME_WAIT 持续存在 2MSL 就能保证在两个传输方向上的尚未被接收或迟到的报文都已经消失
处于TIME_WAIT的一方会自动把遗留的数据舍弃 -
同时也是在理论上保证最后⼀个报文可靠到达
假设最后⼀个ACK丢失,那么服务器会再重发⼀个FIN,
这时虽然客户端的进程不在了
但是TCP连接还在,仍然可以重发LAST_ACK -
确保下次TCP三次握手连接的安全
一旦先退出的一方进入TIME_WAIT状态了,如果要立即启动,此时会bind端口号失败,因为上次的连接还没有关闭,还在处理遗留数据,你不能立即bind,否则如果在三次握手的时候,第一次握手刚完成,突然这个报文就来了,发送到服务器,此时三次握手失败,建立连接也就失败了

这里客户端是采用随机端口号绑定,所以客户端可以立即重启,服务器不能,因为它的端口号绑死了
7.3 如何解决bind失败问题
使用 setsockopt ()设置 socket 描述符的选项 SO_REUSEADDR 为 1 ,
表示允许创建端口号相同但IP地址不同的多个 socket 描述符

这就可以解决bind失败问题,这也是应用角度既要TIME_WAIT也要服务器能立即重启,序号也能解决报文遗留问题,在加上TIME_WAIT就基本能解决报文遗留问题了,所以重启也不担心

我测试了双方各自先退的状态,都是TIME_WAIT,其中第一次是服务器,第二次是客户端,也可以看到客户端到服务器的连接,端口号是随机的,反过来也一样(38212,48474)
7.4 整个TCP建立连接和关闭连接的状态变化
1. 服务端
-
CLOSED -\> LISTEN\]:服务器端调用listen后进入LISTEN状态,等待客户端连接
-
SYN_RCVD -\> ESTABLISHED\]:服务端⼀旦收到客户端的确认报文(应答),就进入ESTABLISHED状态,可以进行读写数据了
-
CLOSE_WAIT -\> LAST_ACK\]:进入CLOSE_WAIT后说明服务器准备关闭连接(需要处理完之前的数据),当服务器真正调用close关闭连接时,会向客户端发送FIN,此时服务器进入LAST_ACK状态,等待最后⼀个ACK到来(这个ACK是客户端确认收到了FIN)
2. 客户端
-
CLOSED -\> SYN_SENT\]:客户端调用connect,发送同步报文段
-
ESTABLISHED -\> FIN_WAIT_1\]:客户端主动调用close时,向服务器发送结束报文段,同时进入FIN_WAIT_1
-
FIN_WAIT_2 -\> TIME_WAIT\]:客户端收到服务器发来的结束报文段,进入TIME_WAIT,并发出LAST_ACK(最后的应答)
间,才会进入CLOSED状态
8. 滑动窗口
滑动窗口大小指的是无需等待确认应答,而可以继续发送数据的最大值
发送方发多少数据由滑动窗口决定

可以把发送缓冲区看成,char类型的一维数组,滑动窗口是缓冲区的一部分,滑动窗口左边表示确定对端已经接收到的报文,这部分空间可以继续被利用了,滑动窗口范围内的报文是可以直接发送的,暂时可以不要应答,滑动窗口大小就是数组下标相减,end - start,滑动窗口后面的数据表示待发送的数据,滑动窗口是基本向右移动的
滑动窗口是流量控制的具体实现方案
滑动窗口大小 = 对方接受能力的大小(16位窗口)
start = 报文确定序号
end = start+报文大小
情况⼀:数据包已经抵达,ACK被丢了

假如这里1到1000的报文应答丢失了,其它报文正常收到,所以其它报文返回的确定序号都是各自的报文尾下标+1,虽然1001这个确序号没有收到,但是2001收到了,序号的定义是序号之前的数据对方主机已经全部收到了,所以返回2001就能确定1001对端一定收到了

2. 数据包直接丢了
1. 快重传

这里如果1001到2000的数据包丢了,没有发送到对端主机,此时其它的报文都要返回确定序号1001,表示1到1000的报文对端主机已经收到,当发送端主机连续三次收到了同样一个"1001"这样的应答,就会将对应的数据1001-2000重新发送,这个时候接收端收到了1001之后,再次返回的ACK就是7001了
(因为2001-7000)接收端其实之前就已经收到了,被放到了接收端操作系统内核的接收缓冲区中
当发送端主机连续三次收到了同样一个"1001"这样的应答,就会将对应的数据1001-2000重新发送,这个机制叫快重传,如果发送端主机没有连续三次收到同一个应答,此时就会触发超时重传
快重传 vs 超时重传
任何报文都要有超时重传,但是快重传是有条件的,快重传本质是满足条件后,只进行快重传,而不在进行原来的超时重传了,提高效率,因为超时重传要费时间,还有序号去重的问题,也就是说超时重传是兜底的,快重传是要满足特定条件的
滑动窗口会向左滑动吗?
不会,因为序号是一次增大的
滑动窗口可以变大,变小,不变吗?
-
变大:假如原本对端的接受缓冲区的大小是64kb, 已占用了54kb,所以本次我的滑动窗口能传的报文大小最多10kb,但是当我传完10kb后,对端突然把这些数据全读走了,再应答,那下次我不就能传64kb了吗,滑动窗口变大
-
变小:对端就是不读取数据,那滑动窗口就越来越小
-
不变:我写了1kb,对端正好也读取了1kb,然后再应答,那滑动窗口就不变
这次就能理解了
start = 报文确定序号
end = start+对方剩余缓冲区大小(16位窗口大小)
如果报文丢失了,怎么办?
-
最左侧丢失
-
如果最左侧的报文没有传到对端主机
-
start下标不变,end下标也不变,就是卡死在1001哪里,除非触发重传,窗口才会向右滑动,就是返回7001的时候,上面说的
-
最左侧的报文应答丢失了
滑动窗口正常工作,因为序号之前的报文已经确认收到了,直接向右移动

最右侧丢失
假如5001到6001报文丢了,此时确认序号返回5001,窗口的start就到5001这个位置,这不右变成最左侧报文丢失的问题了吗
中间报文丢失
假设4001-5001丢失,此时返回4001,然后start继续移动到4001的位置,不又变成最左侧报文丢失的问题了吗?
滑动窗口不会跳过报文应答,序号定义决定的,窗口不能跳跃
tcp发出请求,暂时没有收到应答的时候,必须让对应的报文保存起来,方便后续丢包可以重传,保存在哪里?
保存在滑动窗口内部
超时重传和快重传底层支持是滑动窗口
因为它们重传的报文都是滑动窗口里的报文
滑动窗口,一直向右会不会溢出?
不会,可以理解为环形结构

滑动窗口是正方形的部分,左边是已发送已确认的数据,滑动窗口向右移动是发送用户新写进来的数据,同时之前的已发送已确认的数据也能被用户输入的数据覆盖掉
16位数字最大表示65535,那么TCP窗口最大就是65535字节么?
实际上TCP首部40字节选项中还包含了一个窗口扩大因子M,实际窗口大小是窗口字段的值左移M位
9. 拥塞控制
一个客户端向服务器发送报文,如果出现少量丢包属于正常情况
但如果发了1000个报文,但是丢了998个,此时一定是网络出了问题
TCP不仅考虑了双方主机的问题,还考虑了网络本身的问题,但是前提是硬件无障碍,所有如果大面积丢包,发送方判定网络拥塞问题
此时我要把这些报文都立即重发吗?
客户端和服务器太多了,比如在一个学校,都用校园网,此时一个客户端网络拥塞,那所有客户端一定都网络拥塞,如果所有客户端都立即重发各自的报文,那只会增加网络负载,更加拥堵

怎么解决呢?
TCP引入慢启动机制,以2的n次幂重发报文,拥堵了前期所有客户端发送报文都很少,先看发送一个报文会不会收到应答,如果没有就继续发送一个,后续一样,如果发送n个报文会不会收到应答,如果没有就继续发送n个,但前期是很慢的就是1,2,4,8这样确定,随着指数幂的增大,发送的报文越来越多,收到的应答越来越多,丢包问题很少,此时判定网络不在拥塞了,快速恢复通信过程,因为指数幂变化会越来越大,自然就快速恢复通信过程了

相当于这个图的原理
之前不是说的是发送多少数据,由滑动窗口决定吗?滑动窗口=对方的接收能力吗?
所以为了支持拥塞控制算法(慢启动),提出了拥塞窗口的概念
一个临界值,值以内,网络大概率不拥塞,值以上网络可能拥塞
拥塞窗口本质是一个整形变量,用来在当前计算机内,衡量网络是否会拥堵的一个指标,网络是变化的,就决定拥塞窗口一定要进行更新变化
滑动窗口 = min(对方接受能力,拥塞窗口)
谁小谁是主要矛盾,如果接受能力小,网络再好,也不能发送很多报文,应该由接受能力决定,接受能力很强,网络很差,只能发送较少的报文
这个公式即考虑了网络拥堵问题,又考虑了对方接受能力的问题
但是不能一直指数增长把,就是拥塞窗口这个变量不能一直变大吧,发送的报文的数量,会一直进行指数增长吗?
发送报文的数量肯定是不能一直指数增长的,因为还要受接受能力的控制
拥塞窗口不能一直指数增长,拥塞窗口为什么要一直指数增长呢?

因为它要解决拥塞,快速恢复网络和通信,但是达到一个值(ssthresh)之后,就应该线性增长了,因为它要不断探测网络新的拥塞窗口的值,尽可能让它处于一种稳定状态,所有线性增长,不易幅度过大
所以拥塞窗口要增长,要变化,但是不能一直指数增长

这个ssthresh的初始值一般是16,到底16后,就应该用一个叫拥塞避免加法增大的算法,让拥塞窗口线性增长,直到网络拥塞,吧拥塞窗口重新变成1,然后继续重复刚才的动作,但是新的ssthresh的值就变成12了,使用的是乘法减小的算法,把原来在网络拥塞的拥塞窗口的值/2,就得到了新的ssthresh
与24的交点,表示我们探测出了,当前网络的拥塞窗口
而网络拥塞窗口重新降为1,进行指数增长,就是除了慢启动,本质也是重新探测网络健康,这里起始的慢开始就是,从起始拥塞窗口的ssthresh降到1的
拥塞窗口在线性探测的过程中,会一直增大吗?
理论上会,但实际不会,因为网络总会拥塞
10. 延时应答

比如向缓冲区发送10kb报文,然后对端的缓冲区还剩10kb,然后用了54kb,但是还没读取到应用层,然后对端收到数据,但是不立刻应答,等一会,但这个时间不会超时,就是不会触发超时重传,这期间用户可能把缓冲区的数据全部读走了,然后再应答
客户端有大概率更新出一个更大的滑动窗口,单次发送更多的数据
所以接收方等待一会,就可能告诉发送方,更新一个更大的窗口
这是概率事件,但是客户端和服务端太多了,总有某个客户端一定会更新出一个更大的窗口,就像在一个寝室,有的人的网络就更好一点,但是拥塞是同步的
捎带应答可以提高效率,因为它应答和发送都做了
11. 粘包问题
粘包问题就是多个报文粘在一起了,而且可能不完整
-
如何避免粘包问题呢?
-
归根结底就是⼀句话,明确两个包之间的边界
-
对于定长的包,保证每次都按固定大小读取即可
从缓冲区从头开始按sizeof(Request)依次读取即可 -
对于变长的包,可以在包头的位置,约定⼀个包总长度的字段,从而就知道了包的结束位置
-
对于变长的包,还可以在包和包之间使用明确的分隔符
(应用层协议,是程序员自己来定的,只要保证分隔符不和正文冲突即可) -
对于UDP协议来说,是否也存在"粘包问题"呢?
-
对于UDP,如果还没有上层交付数据,UDP的报文长度仍然在,因为没有发送缓冲区,
一旦交给用户,临时去掉报头,UDP是⼀个⼀个把数据交付给应用层的,就有很明确的数据边界 -
站在应用层的角度,使用UDP的时候,要么收到完整的UDP报文,
要么不收,不会出现"半个"的情况
12. TCP异常情况
进程终止:进程终止会释放文件描述符,仍然可以发送FIN,和正常关闭没有什么区别
机器重启:和进程终止的情况相同
机器掉电/网线断开:接收端认为连接还在,一旦接收端有写入操作,接收端发现连接已经不在了,就会进行reset,即使没有写入操作,TCP自己也内置了⼀个保活定时器,会定期询问对方是否还在,如果对方不在,也会把连接释放
这个过程叫连接的保活,TCP自带,大几十分钟级别的,保活机制由应用层自己完成
另外,应用层的某些协议,也有⼀些这样的检测机制
例如HTTP长连接中,也会定期检测对方的状态
例如QQ,在QQ断线之后,也会定期尝试重新连接
客户端和服务器进程退出,双方的文件描述符就都关闭了,连接也一样,都是四次挥手
关机之前OS要关闭所有进程,所以你关机时会提示有的程序还在启动,是否确认关机
上面说了这么多,总结就是TCP连接总会被释放掉
13. 用UDP实现可靠传输(经典面试题)
参考TCP的可靠性机制,在应⽤层实现类似的逻辑;
例如:
引入序列号,保证数据顺序
引入确认应答,确保对端收到了数据
引入超时重传,如果隔⼀段时间没有应答,就重发数据