传输层协议
可靠传输(大部分场景下,都会优先使用 TCP)
HTTP, 浏览器/app 访问服务器
UDP:和效率更高 (对于性能要求高, 可靠性要求不高的场景下
机房内部的主机之间通信
机房内部, 网络结构比较简单, 带宽通常很充裕
不太容易出现丢包的情况机房内部,通常对于性能要求是很高的(微服务)
1.UDP协议
3.1.1.UDP协议端格式
也可以写成下面这种格式
UDP组成部分分析:
1.载荷:完整的应用层数据包
2.报头:
目的端口和源端口号:就是确定是主机上的哪一个程序
2 个字节,16 bit位一个端口号的取值范围,0->65535实际上,
一般把 1024 以下的端口保留,咱们写代码都是用1024->65535 这个范围的
如果设置 端口号 10w,非法的端口号
长度:UDP的长度(报头+载荷)
上图显示:UDP整个报头的长度是8个字节,
报头又分成4各部分,所以长度有两个字节,
长度属性,也是 2 个字节,表示范围是 0-65535==>64kb
面试表述:
UDP 总长度最大是 64KB
1.UDP 总长度达到 64KB 上限
2.UDP 携带的载荷长度达到 64KB 上限
UDP 报头,只有 8 个字节
8 字节相对于 64KB 来说,非常小的数字
65536-8 =>65528(近似看成 64KB,也是没问题的)
校验和:是防止出现传输过程中的"比特翻转“ 0->1 1->0
光信号,电信号,电磁波收到外界干扰 可能会使高低电平/高低频光信号发生改变
校验和工作原理:
1.发送之前,先计算一个校验和,把真个数据包的数据都代入.把数据和校验和一起发送给对端.
2.接收方收到之后重新计算一下校验和,和收到的校验和进行对比(UDP 发现校验和不一致,就会直接丢弃)
UDP 的校验和使用了 CRC 方式来进行校验(循环冗余校验)
CRC:
把每个字节(除了校验和位置的部分之外),都当做整数, 进行累加.溢出也没关系,继续加最终得到结果
UDP遇到的问题:
64kb太小了,现在数据根本装不下
解决方法:
1.应用层拆包,应用层将数据拆成一个一个小包
工作量是比较大的
写大量的逻辑,实现此处的分包组包功能并且需要进行复杂的验证
如何拆,参考下面的IP协议中的拆包
2.TCP 协议.没有数据包长度的限制
面试:
如果问你UDP如何实现可靠传输 其实就是问下面TCP协议中的可靠传输是如何实现的
3.1.2.UDP的特点
UDP 传输的过程类似于寄信.
1.无连接: 知道对端的IP 和端口号就直接进行传输,不需要建立连接;
2.不可靠: 没有确认机制,没有重传机制; 如果因为网络故障该段无法发到对方,UDP 协议层也不会给应用层返回任何错误信息;
3.面向数据报:不能够灵活的控制读写数据的次数和数量;
什么是面向数据报:
应用层交给 UDP 多长的报文,UDP 原样发送,既不会拆分,也不会合并用 UDP 传输 100 个字节的数据:
如果发送端调用一次 sendto,发送 100 个字节,那么接收端也必须调用对应的-次recvfrom,接收100 个字节; 而不能循环调用 10 次recvfrom,每次接收 10 个字
UDP的缓冲区:
1.UDP 没有真正意义上的 发送缓冲区.调用 sendto 会直接交给内核,由内核将数据传给网络层协议进行后续的传输动作;
2.UDP 具有接收缓冲区.但是这个接收缓冲区不能保证收到的 UDP 报的顺序和发送 UDP 报的顺序一致;如果缓冲区满了,再到达的 UDP 数据就会被丢弃;
UDP使用注意:
我们注意到, UDP 协议首部中有一个 16 位的最大长度,也就是说一个UDP 能传输的数据最大长度是 64K(包含 UDP 首部).
然而 64K在当今的互联网环境下,是一个非常小的数字.如果我们需要传输的数据超过 64K,就需要在应用层手动的分包,多次发送,并在接收端手动拼装;
2.TCP协议
前提:在通讯的过程中,每次传输的数据最小都是一个TCP报头。
2.1.TCP协议格式:
协议组成部分分析:
1.源/⽬的端⼝号: 表⽰数据是从哪个进程来, 到哪个进程去;
2. 32位序号/32位确认号: 作用:确认应答,按序到达, 去重
3. 4位TCP报头⻓度: 表⽰该TCP头部有多少个32位bit(有多少个4字节);
所以TCP头部最⼤⻓度是15 * 4 = 60
4.6位标志位:
前提:我们传输的数据最小都是TCP报头,所以6位标志位其实就是TCP报头中的对应的bit位置为1。
问题:为什么要有标志位?
其实接收方:收到的TCP报文一定会存在不同的类型针对不同报文类型,按接收方要有不同的做法,所以标志位就是为了区分不同的TCP报头类型
(SYN和ACK在建立连接中有详细的讲解)
1.SYN: 请求建⽴连接; 我们把携带SYN标识的称为同步报⽂段
2.ACK: 确认号是否有效
(FIN在建立连接中有详细的讲解)
3.FIN: 通知对⽅, 本端要关闭了, 我们称携带FIN标识的为结束报⽂段
4. PSH: 催促标志位:发送方给接收方发了数据中带有这个标志位,接收方就会尽快的这个数据 read 到应用程序中.
(RST在建立连接失败中有详细的讲解)
5.RST: 对⽅要求重新建⽴连接; 我们把携带RST标识的称为复位报⽂段
6. URG: 紧急指针是否有效
tcp保证可靠性的 ->按照序号 - 按序到达接受缓冲区。
如果我们有数据,想被优先读取优先处理。就发送一个URG类型的TCP数据报,
16位紧急指针就是和URG相互搭配使用,因为此时在接受缓冲区TCP是按序排序,当前报文的有效载荷中,”特定偏移量处",有紧急数据
5.16位窗⼝⼤⼩: 在流量控制中有作用
6.16位校验和: 发送端填充, CRC校验. 接收端校验不通过, 则认为数据有问题.
此处的检验和不光包含 TCP⾸部, 也包含TCP数据部分.
7. 16位紧急指针: 标识哪部分数据是紧急数据
8. 40字节头部:滑动窗口中能使用到
9.数据:应用层数据包
此时URG就会置为1
TCP 正常来说,按照序号顺序,发送和接收
紧急指针,相当于"插队
跳过前面的数据,直接从某个指定的序号来开始read
10.保留6位:UDP 的问题,长度不够.又不能扩展
TCP 的设计者就考虑到这样的问题,TCP 报头中就预留了一些"保留位”
(现在先不用,但是占个位子.)
2.2.确认应答
TCP 的特性:可靠性
网络通信,是非常复杂此处的可靠性,不是说,A 给 B发一个消息,B100% 能收到
而是 A 给 B发了消息之后,尽可能的让 B 收到
A 能够知道 B 是否收到了
这个可靠性主要就靠:确认应答,超时重传
保证可靠性的关键前提是,发送方知道接受方已经收到了我的数据
此时就引入了 标志位中的 ”应答报文“,(ackknownledge)简称:ack ,、
发送方收到 ack就知道我发的数据已到达
确认应答的工作原理:
1.首先:TCP将载荷部分的每一个字节部分进行编号
2.主机A向主机B发送数据
确认序号的含义:
1.<1001 的数据都已经确认收到了
2.接下来你要从 1001 开始给我发送
序号的填法:填写载荷部分第一个字节的序号
确认序号的填法:将受到数据的载荷部分最后一个字节的序号+1,在进行填入
3.数据到达主机B的时候,因为我们此时引入了序号,所以可以利用序号进行排序
TCP 在接收方这里会安排"接收缓冲区"(内存,操作系统内核里通过网卡读到的数据,先放到接收缓冲区中,后续代码里调用 read也是从接受缓冲区来读的
根据序号来排序,序号小的在前面,大的在后面确保前面的数据已经到了,然后 read 才能接触阻塞如果是后面的数据先到,read 继续阻塞,不会读取到数据,所以序号的存在也就保证了数据的有序性
此时我们就知道这32为序列号原来就是为了确认应答做准备的,但是根据应答,我只需要返回一个序列号就行了,为什么要这只两个32位序列号?
因为此时的服务器并不是只做应答捎带应答了即需要对对方报文做确认所以自己的报文也有序号。
example:
client:1+1等于什么 ,听到我的问题了吗?
server:听到了 + 等于2。
此时的 “听到了”:32位确认序列 “等于2”:32位序列
2.3.超时重传
超时重传针对 "丢包问题"
为什么会丢包:
数据报经过某个路由器,交换机转发的时候,该路由器/交换机已经非常繁忙了,导致当前需要转发的数据量超出路由器/交换机的转发能力上限
丢包的两种情况:
1.A给B发送的数据丢了
2.B返还给A的ack丢了
丢包造成的影响:
不管是丢包的第一种情况还是第二种情况,因为此时A都是没有收到主机B发来的ack,都会造成主机A进行重发数据,
所以B又会出现两种情况,
接受到一份数据
接收到两份相同的数据
B 收到了两份一样的数据
如果 tcp 不处理,可能会使应用层读到两次一样的数据
去重操作:
tcp 会在内部进行去重操作
接受缓冲区
就可以根据序号,在接受缓冲区中找一下如果存在,就直接丢弃如果不存在,才放进去
重传机制:
达到等待时间的上限,还没有收到 ack
A 就认为传输中发生丢包了
最理想的情况下,找到一个最小的时间,保证"确认应答一定能在这个时间内返但是这个时间的长短,随着网络环境的不同,是有差异的如果超时时间设的太长,会影响整体的重传效率;如果超时时间设的太短,有可能会频繁发送重复的包;
TCP 为了保证无论在任何环境下都能比较高性能的通信,因此会动态计算这个最大超时时间.
1.Linux 中(BSD Unix 和 Windows 也是如此),超时以 500ms 为一个单位进行控制,每次判定超时重发的超时时间都是 500ms 的整数倍.
2.如果重发一次之后,仍然得不到应答,等待 2*500ms 后再进行重传.
3.如果仍然得不到应答,等待 4*500ms 进行重传,依次类推,以指数形式递增
4.累计到一定的重传次数,TCP 认为网络或者对端主机出现异常,强制关闭连接
2.4.连接管理
2.4.1.建立连接
连接:是一个虚拟的连接,就是让对方都保留自己的信息。
6位标志位:
synchronized(syn) 同步:在这里就是建立连接,握手过程种使用的标志位。
acknowledgment(ack):表示报文是一个应答报文,ack标志位通常是设置1为1,要不就是应答,要么就是应答+数据
TCP 中的同步,:
"数据上的同步”A 告诉 B,接下来我要和你建立连接.就需要你把我的关键信息(端口号),
保存好,同时你也把你的信息同步发给我,接受到对方的关键信息后序号发给对方一个ack
建立连接使用三次握手来实现:
三次握手, 一定是客户端主动发起 syn(第一次握手, 一定是客户端开头的)
第一次:A给B发送一个syn
第二次:B给A发送一个syn+ack(这里的 syn 和 ack都是内核负责的.和用户代码无关可以保证是同一时机)
第三次:A给B发送一个ack
为什么要进行合并操作:
合并操作,是有效能够提高传输的效率的
网络传输过程,是要能够进行"封装和分用的
客户端服务器状态分析:
注意:所有状态变化的moment都是以自己的视角 ,只要我发出,我接受到了,就会变化,管你接受没接收到,我就变成这种状态,所以在不同的视角,client和server完成挥手的进度是不同的
1.CLOSED:
不存在的状态,TCP还没有进行连接
2.LISTEN:
启动服务器,new ServerSoket的时候就会进入这种状态,相当于饭店已经开门做好了,接客的准备
3.ESTABULISHED:
已经建立连接,随时可以发送数据,
建立连接失败的情况:
有了状态变化的前提,当第三次握手的时候,但是ACK应答报文丢失了,client视角:已经连接成功, server:连接失败。
此时就会造成连接建立是否成功认知不一致!!!
当client向server发送消息的时候,server就会”一脸疑惑“,所以client就会向server发送一个RST类型的TCP报头
三次握手的作用:
1.三次握手, 相当于"投石问路”
先初步的探一探网络的通信链路是否通畅.(网络通畅是可靠传输的前提条件)
2.验证通信双方的发送能力和接收能力是否也正常(验证client和server的全双工)
当B第一次收到A的信息:A的发送功能是可以的
当A收到B的信息:B的接受和发送功能是可以的
当B第二次收到A的信息:A的接受功能是可以的
3.三次握手还可以协商一些信息
TCP要协商最重要的一个事情就是:传输的数据从序号多少开始
1.初始的序号一般不是从0开始
2.连续的两次连接的初始序号一般相差很大
举个例子:
假设有两次连续的连接,第一次的传输的起始序号是100000,
第二次传输的起始序号是800000
1.在第一次传输的时候,有个数据因为延迟,在第一次传输结束,创建第二次连接的时候才达到主机B
2.因为有了之前的协商,知道我的序号从哪开始,知道这个晚来的数据不是这次连接的,所以就可以将这个数据给剔除
2.4.2.断开连接
6位标志位:
FINISH:(FIN):通知对方,本端要关闭了,我们称携带FIN标识的为结束报文段
四次挥手流程:
四次挥手, 客户端和服务器, 都可以主动发起 FIN(就看是谁先调用 close)
第一次:A给B发送一个FIN
第二次:B给A发送一个ACK
第三次:B给A发送一个FIN
第次四:A给B发送一个ack
上面的第二次发送和第三次发送是否可以合成为一次?
不能:
ack 是内核控制返回的. 内核收到 FIN,第一时间返回 ack,和你应用程序代码无关
第二个 FIN 则是代码中调用 socket.close 才会触发的(进程结束, 也能触发)
第二个 FIN 的时机和 ACK 的时机很可能不是同一时机
代码上的体现:
当客户端发送第一次FIN的时候,此时就会碰到代码1,
此时就会个客户端发送ACK,
但是服务器还是要执行完代码2,
到达代码3才可以发送第二个FIN
客户端服务器状态分析:
TIME WAIT:谁是主动发起 FIN 的一方,就会进入到 TIME WAIT
CLOSE WAIT: 谁是被动发起 FIN 的一方,就会进入 CLOSE WAIT
为什么需要 TIME_WAIT 状态?(两大核心原因)?
1. 防止旧连接的延迟数据包干扰新连接
- 场景:假设客户端和服务器关闭连接后,立即建立一个新的同名连接(相同源 IP、端口和目标 IP、端口)。
- 风险:旧连接中可能存在延迟到达的数据包(如未及时到达的 FIN 或数据段),若直接建立新连接,这些 “旧数据” 可能被新连接误接收,导致数据混乱。
- TIME_WAIT 的作用:等待足够时间(2 倍 MSL,MSL 为最大段生存时间,通常约 2 分钟),确保旧连接的所有数据包都已过期失效,避免干扰新连接。
2. 确保最后一个 ACK 成功到达对方
- 场景:客户端发送最后一个 ACK 后,若该 ACK 在网络中丢失,服务器未收到 ACK 会重发 FIN 包。
- 风险:若客户端不维持 TIME_WAIT 状态,直接关闭连接,则无法响应服务器重发的 FIN 包,导致服务器无法正常关闭连接(一直处于
LAST_ACK
状态)。 - TIME_WAIT 的作用:在 TIME_WAIT 期间,客户端仍能接收服务器重发的 FIN 包,并重新发送 ACK,确保双方连接正常关闭。
四次挥手出现丢包的情况:
正常的情况:
1.如果第一个 FIN 丢了,A 就无法再规定时间内拿到ACK, 于是 A 重新发送 FIN
2.如果是第一个 ACK 丢了,同上
3.如果是第二个 FIN 丟了呢,B 也再次重传就可以了在第二个 FIN 到达 A 之前,A 这里的连接肯定是存在的,就能够及时的处理 ACK
4.但是如果第二个ACK丢了,此时B就会以为自己的FIN没有到达A,进行重发FIN,
所以此时的A不可以一收到FIN发出ACK就进行CLOSED.
此时A需要进行一段时间的休眠,就是为了等待B是否会不会进行重发FIN
异常的情况:
此时A不等了,但是B还是没有收到A的ACK
1.站在 A 的视角, 此时 A 给 B 已经把 FIN 发了很久了,B 也没有进行后续的挥手操作A 就会主动释放连接(也就是把 B 的核心信息删了)
2.B 这边,由于代码逻辑都有 bug, 这里的连接,暂时存在 (还会保存对方的信息,此时也没法进行正常的数据通信了)
三、TIME_WAIT 的持续时间:2MSL(Maximum Segment Lifetime)
- MSL:数据包在网络中的最大生存时间,RFC 建议为 2 分钟(不同系统可能调整)。
- 2MSL 的意义:
- 若 ACK 丢失,服务器重发 FIN 包的时间 + 客户端再次发送 ACK 的时间,总和不超过 2MSL。
- 确保旧连接的所有数据包在 2MSL 内过期,避免被新连接接收。
2.5.滑动窗口
上面讲了TCP中的确认应答,超时重连,连接管理都是为了TCP的可靠性,
但是代价就是自身的效率下降,
此时滑动窗口就是为了,挽救自身的效率而做的
滑动窗口的本质:其实就是发送缓冲区的一部分。
整个发送缓冲区就可以分成下面三部分:
1.已发送已确认:这部分数据就无效了,这部分空间可以被利用可以被覆盖
2.可以直接发送,等待应答中
3.带发送数据。
问题:1.可以向左滑动吗?
不会
问题2.滑动窗口,可以变大吗?可以变小吗?可以不变吗?可以为零吗?
都是可以的,都是由对方的接受能力决定的
没有引进滑动窗口的机制:
引入了滑动窗口的机制:
流程:
1.一开始是直接将一组数据进行发送
2.后序,接受到一个ack就发送一条数据(如果等待一组的ack都到达,这样更加消耗时间)
问题:
如果2001比1001的ack先到达是怎么滑动的?
如果是 2001 ack 比 1001 先到窗口直接往后走两个格子就可以了
确认序号的含义: 该序号之前的数据,都确认收到了
2001 ack 能够涵盖 1001 的含义
滑动窗口中的丢包问题
丢包问题又分为下面三种情况:
a.最左侧丢失
b. 中间报文丢失
c.最右侧丢尖
先考虑最左侧的报文丢失
情况一:ack丢失
此时不用管,因为后面的ack就包含了之前的数据已传达
情况二:数据丢失
此时1000这个数据丢失,主机B会发三次你要将1000的数据给我,
此时就只会重发1000这个数据,这种操作叫做 快速重传
快速重传:
只是谁丢了,重传谁,其他已经收到的数据无需重传整个重传过程速度很快的
(滑动窗口下的,超时重传的变种操作)
超时重传vs快速重传
超时重传:传输的数据量少,没有构成滑动窗口批量传输的形式,(但是有收到3个相同的确认应答的触发条件)
快速重传:传输数据量多,形成滑动窗口
再看中间报文丢失和最后的丢失,其实都可以转化成最左侧这种情况,因为我已经知道左侧的数据已经到达,只需将左窗口向又移动到未确认的位置即可。
2.6.流量控制
在TCP协议格式中的16位窗口大小其实就是:自己的接收缓冲区剩余空间的大小。
为什么要有流量控制?
要知道一个TCP数据是经过cpu 中大量的算力和电力形成的,当我将我形成的数据传给你的时候,你说因为你接受能力不行我要将你传给我的数据给丢弃。这是一种资源的浪费,所以就需要有这个16位窗口大小,来告诉对方我现在的接受能力,让对方的传输速率做出调整。
滑动窗口,窗口越大, 效率就越高,但是不能无限大,太大了会影响到可靠性
接收方的处理能力是有上限的,和你写的代码有关系
举个例子:
1.蓄水池:应用程序,需要从接受缓冲区中, 读取数据就相当于放水
读了一个字节,这个字节就可以从接受缓冲区删除了
2.蓄水的速度:就是发送方发送的速度
3.应用程序读取的速度:看你代码是咋写的了
如果水位已经满了,此时继续蓄水就会冒(丢包)
流量控制就是给发送方踩刹车,让它发的慢点
流量控制可以让接收方,根据自身处理数据的速度,反馈给发送方,限制发送方发送的速度在 ack 中,依赖一个特殊的属性,"窗口大小'
接收方接收缓冲区的剩余空间大小填入到这个属性中
发送方就会按照这个数字来重新设定发送的滑动窗口大小(滑动窗口的大小,是动态变化)
问题:那么此时滑动窗口最大值是64kb?
不是
因为在选项中有一个特殊的属性:窗口扩展因子
窗口大小=16位窗口大小<<窗口扩展因子
流量控制的机制:
1.假设A给B发送1000数据,B给A的应答中包含了自己的窗口大小为3000.
2.那么主机A就一次性发送3000的数据,假设此时应用程序没有读取任何数据
3.那么当数据都到达的时候,B给A的ack中就会告诉我的窗口大小为0
4.那么此时A就不会发信心给B
5.但是一直不给B发数据也不是一个事,此时就引入“窗口数据包”
6.A给B发送一个窗口数据包,只是为了触发ack主动的询问一下消化的怎么样,窗口大小为多少
2.7.拥塞控制
虽然 TCP 有了滑动窗口这个大杀器,能够高效可靠的发送大量的数据. 但是如果在刚开始阶段就发送大量的数据,仍然可能引发问题.
因为网络上有很多的计算机,可能当前的网络状态就已经比较拥堵.在不清楚当前网络状态下,贸然发送大量的数据,是很有可能引起雪上加霜的.
TCP 引入 慢启动 机制,先发少量的数据,探探路,摸清当前的网络拥堵状态,再决定按照多大的速度传输数据;
拥塞控制的机制:
阶段 1:慢启动(Slow Start)
- 触发条件:连接建立后,或网络拥塞解除后。
- 核心逻辑:
- 拥塞窗口
cwnd
指数级增长(每收到一个 ACK,cwnd += 1
)。 - 例如:
cwnd=1
→ 收到 ACK →cwnd=2
→ 收到 2 个 ACK →cwnd=4
→ 以此类推。
- 拥塞窗口
- 切换条件:当
cwnd >= ssthresh
时,进入拥塞避免阶段。
阶段 2:拥塞避免(Congestion Avoidance)
- 触发条件:
cwnd
达到ssthresh
(或网络未拥塞时)。 - 核心逻辑:
- 拥塞窗口
cwnd
线性增长(每收到一个轮次的 ACK,cwnd += 1
)。 - 例如:
cwnd=16
(假设ssthresh=16
)→ 每轮次增长 1 →cwnd=17
、18
...
- 拥塞窗口
- 意义:避免
cwnd
指数增长导致网络拥塞,改为缓慢增加发送量,试探网络承载能力。
阶段 3:拥塞发生(丢包触发)
- 触发条件:
- 出现超时重传(发送方未及时收到 ACK,认为网络拥塞)。
- 或收到3 个重复 ACK(接收方连续收到相同 ACK,暗示数据包丢失)。
- 处理逻辑:
- 超时重传( Tahoe 版本):
- 新
ssthresh = cwnd / 2
(乘法减小)。 cwnd = 1
,重新进入慢启动。
- 新
- 3 次重复 ACK( Reno 版本):
- 新
ssthresh = cwnd / 2
(乘法减小)。 cwnd = ssthresh + 3
(快速恢复,因 3 个重复 ACK 说明有 3 个数据包已被接收)。
- 新
- 超时重传( Tahoe 版本):
阶段 4:快速恢复(Fast Recovery)
- 触发条件:收到 3 个重复 ACK(仅 Reno 及后续版本支持)。
- 核心逻辑:
ssthresh
设为当前cwnd / 2
(乘法减小)。cwnd
设为ssthresh + 3
(因 3 个重复 ACK 说明有 3 个数据包已被接收,无需完全回退到慢启动)。- 每收到一个重复 ACK,
cwnd += 1
(快速恢复丢失的数据包)。 - 当收到新 ACK(确认丢失的数据包已重传),
cwnd = ssthresh
,重新进入拥塞避免。
拥塞控制的总体思想:
先按照小的窗口(小的速度) 先发着如果发的时候很顺利,不丢包,加大速度出现丢包,减小速度
又不丢包了,继续加大速度又丢包了,继续减小速度,动态平衡
流量控制:依据是接受方的处理能力,进行限制
拥塞控制:依据是链路传输层的转发能力(网络方面的原因),进行限制。
问题:窗口的大小到底是谁说的算
谁小谁老大,谁小以谁为基准
2.8.延时应答
默认情况下,接收方都是在收到 数据报 第一瞬间,就返回 ack
但是可以通过延时返回 ack 的方式来提高效率(不是100%,要看读取数据和传输数据的速度比)
当读取速度比传输的速度快的时候:
那么所有的包都可以延迟应答么?
• 数量限制: 每隔N个包就应答⼀次;
• 时间限制: 超过最⼤延迟时间就应答⼀次;
具体的数量和超时时间, 依操作系统不同也有差异; ⼀般N取2, 超时时间取200ms;
2.9.捎带应答
TCP 已经有了延时应答了,基于延时应答,
引入"捎带应答"返回业务数据的时候,顺便把,上次的 ack 给带回去
如果没有延时应答,
返回 ack 的时机和返回响应的时机,就是不同时机
原因:
把两个包合成一个就能起到提高效率的作用
涉及之前的封装,分用
2.10.面向字节流
UDP 来说,就不存在这样的问题以 UDP 的数据包为单位读写的,
一个 UDP 数据包承载一个应用层数据包,
每次 receive 得到的结果就是一个完整的应用层数据包
面向字节流,就会引发一个问题==>粘包问题
粘的是"应用层数据包”
通过字节流方式传输,很容易混淆包和包之间的边界
从而接收方无法去区分从哪里到哪里是一个完整的应用层数据包
解决方法:
上述问题,在 tcp 的层次上,无解需要站在应用层解决.
定义好应用层协议,明确包之间的边界
1.约定包和包之间的分隔符(包的结束标记)
echo server 采用的办法.约定 \n 作为结束标记
2.约定包的长度~比如约定每个包开头 4 个字节,表示数据包一共多长
2.11.异常情况处理
1.某个进程崩溃了
资源释放正常
进程崩溃,和主动退出,没有本质区别, 进程释放
FIN,触发四次挥手
进程虽然没了,但是 TCP 的连接信息还存在此时四次挥手还是可以正常进行的
2.主机关机了
分为两种情况
主机关机了正常流程的关机本质上还是会先杀死所用的用户进程
情况一:
关机也需要一定的时间,如果一定时间内,四次挥手,挥完了,就和正常一样了
情况二:
没有挥完
假设 B 的 FIN 来的太迟了,导致 A 已经关机完成了,
意味着 B 的 FIN 不会有 ACK
B 重传 FIN,也没有 ACK经过几次之后,认为对端出现严重问题,
此时 B 主动放弃连接(B 把保存的 A 上的信息就删掉了)
此时资源还是可以释放的
3.主机掉电
分两种情况
情况一:接受方掉线
1.B发送数据给A,但是A没有给B返回ack
2.就会激发B的重传机制
3.重传到一定次数就会触发“重置TCP连接“
4.B主动给A发送一个”复位报文“(RCT, 表示我们重头开始)
5.如果还是没有接收到ack,那么B只能单方面的释放连接
情况二:发送方掉线
1.B不知道A是什么情况,只能等待A一会
2.等了一会后,B会主动给A传输一个”心脏包“(没有携带载荷数据),为了触发ack
3.如果对方有心跳,继续正常等待如果没心跳, 就只能通过 rst 尝试还是不行,就只能单方面释放连接
发送心脏包的行为-->"保活机制"
问题:TCP 的心跳, 周期太长了,分钟级别的
虽然 TCP 内置了心跳包,实际开发中,通常还是会在应用层重新实现心跳包效果
现在通常希望,秒级,甚至毫秒级,就能发现对端是否,从而触发一些后续的操作
4.网线断开
站在 A 的视角,就是和刚才上面"接收方掉电"是一样的情况
站在 B 的视角,就和刚才上面"发送方掉电"是一样的情况
经典面试题:用 UDP 实现可靠传输
参考 TCP 的可靠性机制,在应用层实现类似的逻辑;
例如:
引入序列号,保证数据顺序;
引入确认应答,确保对端收到了数据;
引入超时重传,如果隔一段时间没有应答,就重发数据: