目录
核心机制4:滑动窗口
滑动窗口的丢包问题
情况一:数据包已经抵达,ACK 被丢了
情况二:数据包就直接丢了
[面试题]超时重传 vs 快速重传
答疑
核心机制5:流量控制
答疑
核心机制6:拥塞控制
拥塞控制vs流量控制 哪个说的算??
小结
书接上文:Java EE:8.网络原理- TCP / IP(第二弹)~~
核心机制4:滑动窗口
我们说 TCP 会保证可靠性,但是这也会付出代价,那就是效率下降~~
闲聊:对于滑动窗口,很多老铁表示 DNA 动了,但其实算法中的“滑动窗口”是出自 TCP 的~~

既然这样一发一收的方式性能较低,那么我们一次发送多条数据(批量传输),就可以大大的提高性能(其实是将多个段的等待时间重叠在一起了)

窗口大小:不需要等待,能够连续发送的最大数据量~~,上图的窗口大小就是 4000 个字节(四个段)
下一组是怎么发的呢??
1.等这一组所有的 ACK 都回来,再发第二组
这么做,花的时间会更长~~不采纳❌
2.收到一个 ACK,就发下一条✔
发送前四个段的时候,不需要等待任何 ACK,直接发送
收到第一个 ACK 之后,滑动窗口向后移动,继续发送第五个段的数据,以此类推
操作系统内核为了维护这个滑动窗口,需要开辟发送缓冲区,来记录当前还有哪些数据没有应答,只有确认应答过的数据,才能从缓冲区删掉

窗口越大,批量发的数据越多,效率就越高
但是窗口也不能无限大,太大也会影响到可靠性
滑动窗口是在可靠传输的基础上,提高效率,这种做法只是亡羊补牢,因为引入可靠性,会使效率产生折损,引入滑动窗口,只是让折损更小,不可能效率比 UDP 这种还高~~
答疑
Q1:有可能中间的先返回 ACK 啊
如果 2001 ACK 比 1001 先到
窗口直接往后走两个格子就可以了~~
因为确认序号的含义:该序号之前的数据,都确认收到了~~
2001 ACK 能够覆盖 1001 的含义:<2000 的数据都收到了,自然也包括 1~1000 的数据了~~
Q2:序号不是可以保证发送的先后顺序吗??
序号是保证你应用程序 read 数据的先后顺序
不是数据到达对方的顺序~~
B 给 A 返回多个 ACK,先发 1001 后发 2001,但是很可能 2001 先到~~
如果 2001 先到,那么 1001 到不到都无所谓了,因为 2001 到了,已经包含 1001 已经到了~~
Q3:序号表示读的顺序就是序号大的都收到了说明序号小的肯定读到了
虽然措辞有点问题,但是可以这么理解~~
操作系统内核收到,和应用程序 read 其实关系不大
操作系统收到数据是放到接收缓冲区了,而 read 应用程序调用,是另一回事儿~~
只要你数据被对方内核收到了,就会立即返回 ACK(也是内核完成的)
滑动窗口的丢包问题
那么如果出现了丢包,如何进行重传?这里分两种情况讨论
情况一:数据包已经抵达,ACK 被丢了

上图中 6 个丢了3个,丢包率达到50%了,已经是非常恐怖的数字了,网络已经有严重问题了,但是这种情况下,部分 ACK 丢了并不要紧,因为可以通过后续的 ACK 进行确认(后一个 ACK 能够涵盖前一个 ACK 的含义~~)
情况二:数据包就直接丢了

快速重传
①如上图,当 1001~2000 丢失之后,发送端 A 会继续发 2001~3000、3001~4000……,但是接收端 B 就不会发“下一个是 3000”!!!虽然后续的 2001~3000 收到了,但是由于 1001~2000 丢包了,所以接收端 B 仍然会向发送端索要 1001 的数据,告诉发送端 A “下一个是 1001”
②而这边发送端 A 根据接收端 B 的回应就会发觉,“我咋发啥,接收端 B 都是要 1001 呢??”,于是发送端 A 连续三次收到 “下一个是 1001” 之后,就会意识到 1001~2000 怕是丢包了!!发送端 A 就会重新传输 1001~2000
③1001~2000 重传之后,此时的确认序号就是 7001,因为刚才 2001~7000 这些数据都收到了,就差 1001~2000 了,通过重传,一下子就把缺失的拼图补上了,补上之后,继续从 7001 往后传输就可以了~~
④以上就是“快速重传”:只是谁丢了,就重传谁,其他已经收到的数据无需重传,整个重传过程速度是很快的(滑动窗口下的超时重传的变种操作)
以上内容就类似于,有个妹子要开车带着好闺蜜去参加婚礼,突然车打不着火儿了~~
这时候妹子给男朋友打电话:
妹子:车打不着火儿了!!
男朋友:看一下是不是电瓶没电了~~
妹子:一会儿迟到了咋办??
男朋友:看一下是不是电瓶没电了~~
妹子:这可是我最好的闺蜜呀!我要是迟到了她一定会难过的😭~~
男朋友:看一下是不是电瓶没电了~~
妹子:你咋一直重复一句话,你是不是不爱我了🤬!!
男朋友:看一下是不是电瓶没电了~~
妹子:解决了,确实是电瓶没电了~~
男朋友:那就继续走呗~~
[面试题]超时重传 vs 快速重传
这两个机制是矛盾的嘛??
其实不是,这两个机制是针对不同情况下的重传机制~~
就好比运动中的 牛顿经典力学 vs 相对论 之间的关系,两者都对,只不过牛顿经典力学适用于宏观低速,而相对论适用于 微观高速~~
超时重传:传输的数据量少,没有构成滑动窗口批量传输的形式~~
快速重传:传输数据量多,形成滑动窗口~~
答疑
Q1:快速重传会早晨数据的混乱吗??
你说的是顺序吗??
其实就好比于👇

本来就已经留了位置了,此处就可以直接把后来收到的 1001~2000 填到空位上就行了
后续应用程序 read 的时候,还是顺序读~~
像队列,但又不是纯粹的队列,因为每个数据都是有序号的,可以根据序号准确计算出数据应该放到哪个位置,以及前面需要空多少~~
Q2:意思就是说位置一直给你留着,你为社团坐完牢,位置还是你的~~
对的,就这意思~~
Q3:感觉有点像数组~~
就是字节数组!!
核心机制5:流量控制
我们通过上面知识知道,滑动窗口的窗口越大,效率就越高,但是也不能无限大,太大了也会影响到可靠性:接收方的处理能力是有限的(这一点与你写的代码有关系)~~
接收端处理数据的速度是有限的,如果发送端发的太快,导致接收端的缓冲区被打满,这个时候如果发送端继续发送,就会造成丢包,继而引起丢包重传等等一系列连锁反应
就好比我们小学经常提到的蓄水池问题~~

这里蓄水的速度就是发送方发送的速度
放水的速度就是应用程序读取的速度
如果水位以及满了,此时继续蓄水,就会冒出来(丢包)
因此 TCP 支持根据接收端的处理能力,来决定发送端的发送速度,这个机制就叫做流量控制(Flow Control)
流量控制就相当于给接收方踩刹车,让他发的慢点
流量控制可以让接收方,根据自身处理数据的速度,反馈给发送方,限制发送方发送的速度~~
答疑
Q1:这个得根据客户端性能来定吧??
不是根据客户端,而是根据接收方的性能,这个接收端可能是客户端,也可能是服务器~~
Q2:服务器和客户端都有这样的接收缓冲区是吧?
每个 Socket 对象,都会对应到一组接收缓冲区(和发送缓冲区)~~
一个程序中创建了 N 个 socket 对象就有 N 组这样的缓冲区~~
Q3:内存缓冲区不是一直在那吗?为啥还需要调用 socket?
给大家详细讲一下吧~~
你每次在 Java 代码中创建 Socket 对象👇
Socket clientSocket = serverSocket.accept();
就会在 内核 的文件描述符表中多一个表项,这个表项就指向另一个文件对象,在文件对象中就同时有发送缓冲区和接收缓冲区(这两个都可以看成是字节数组)

此处说的是 TCP 的发送缓冲区和接收缓冲区,是在内核中,但不叫“内核缓冲区”~~
Q4:内核缓冲区是公用的吧,创建了就会分配其中一块?
你说的是内核管理的空闲内存吗??
这个不是此处谈到的 TCP 的发送缓冲区/接收缓冲区~~
Q5:缓冲区是介于 CPU 和内存之间的,用于提高效率
那你说的这个也不叫内核缓冲区呀,这不是缓存吗??
这是缓存(cache),也不是缓冲区(buffer)
这是一个硬件设备,跟缓冲区没关系,是这个东西👇

我们使用术语要准确~~
我们此处谈到的 TCP 的发送缓冲区 和 TCP 的接收缓冲区,是两个专业术语,跟缓冲区是两回事儿,缓冲区是另一个术语
操作系统内存管理的时候,分配空闲内存的“自由链表”是另一个术语
高速缓存是另一个术语
一码是一码儿,大家不要混淆呀~~
Q6:可以在说一下内核缓冲区是什么吗??
压根儿没有这个词,兄弟们~~
在我印象中,“内核缓冲区”不是一个专业术语
缓冲区(buffer)是一个广义的概念,一般就是一个内存空间,这个空间通常用来协调不同速度的硬件设备
我们之前讲 IO文件 的时候,写文件的时候就是先写到缓冲区,然后统一写入到硬盘上~~
应用程序中可以有 N 个缓冲区,内核中也可以有 M 个缓冲区
(难道是你们把 内核中的 M 个缓冲区叫做“内核缓冲区”吗??😂)
我们此处的 TCP 接收缓冲区 和 TCP 接收缓冲区是特指的两个在 TCP 通信过程中和 Socket 绑定在一起的缓冲区,确实也是在内核里的,但是不能叫内核缓冲区
Q7:内核里的缓冲区是干啥用的??
用途非常多,很多很多地方就会涉及到~~
Q8:内核态是在接收缓冲区吗??
正确的表述应该叫做“接收缓冲区是在内核里”
代码执行在内核态的时候,才能访问到 TCP 接受缓冲区
Q9:那缓冲区和缓存啥区别??
这是两个完全不相干的概念,都能提高效率,但是本质是不一样的~~
缓冲区(buffer):把低效操作的次数变少了~~
把多次低效的操作,合并成一次~~既可以用来读,也可以用来写数据~~
好比“嗑瓜子”,把瓜子皮儿攥一把,统一丢一次~~
缓存(cache):也是把低效操作的次数变少,把之前读过的数据,放到一个更近更方便的地方,便于下次来拿~~
好比“坐高铁”,把身份证从皮箱拿到上衣口袋,方便后续随时拿身份证~~
流量控制的实现,在 ACK 中,依赖一个特殊的属性“窗口大小”👇

这个窗口大小就是接收方接收缓冲区的剩余空间大小,在之前蓄水池的比喻中的这个地方👇

接收方接收缓冲区的剩余空间大小填入到这个属性中,发送方就会按照这个数组来重新设定发送的滑动窗口大小(滑动窗口的大小是动态变化的,如果剩余空间大,就多发点,剩余空间小,就少发点~~)
问题就来了,我们之前学 UDP 时的 64kb 问题~~
是否滑动窗口大小,最大数值就是 64kb 呢??
TCP 贼溜贼溜的,在 UDP 踩过坑之后,TCP 已经长记性了~~
这里就有一个特殊的属性:窗口扩展因子~~

我们实际上表示的窗口大小=窗口大小<<窗口扩展因子(窗口大小这个数字左移窗口扩展大小个 bit 位)
比如我窗口大小为 64kb,窗口扩展因子为 2 时,整个窗口大小= 64kb × 2² = 256kb
我们需要注意的是,这是一个移位操作,意味着整个大小会呈指数级增长~~
所以我们结合着窗口扩展因子就可以使16位窗口大小这个地方表示一个非常非常大的数字~~
当然,如果不足 64kb 的话,窗口扩展因子就可以不写,直接拿16位窗口大小的值来算~~
我们可以通过图解来体会一下👇

接收端将自己可以接收的缓冲区大小放入 TCP 首部中的“窗口大小”字段,通过 ACK 段通知发送端
接收端一旦发现自己的缓冲区快满了,就会将窗口大小设置成一个更小的值通知给发送端
发送端接收到这个窗口之后,就会减慢自己的发送速度
如果接收端缓冲区满了,就会将窗口置为0,这时发送方不再发送数据(继续发送就会丢包),但是一直不发的话,就无法得到 ACK,也就相当于 B 接收缓冲区后续有空余了,A 也不知道呀~~
所以 TCP 这里就有一个很巧妙的地方,发一个窗口探测包(这个数据包没有任何业务数据),只是为了触发 ACK,主动询问一下,接收方现在咋样了~~
答疑:这些图解好清晰,在哪里弄的??
这些图都来自《图解 TCP/IP 协议》,这里面有很多插图,图都非常好,不像其他 TCP/IP 协议的书那么晦涩难懂,很推荐大家去看一看~~

与之类似的《图解 HTTP 协议》也是非常好的~~
核心机制6:拥塞控制
流量控制是依据接收方处理能力,进行限制的
拥塞控制则是依据传输链路的转发能力,进行限制的
因为两个设备不是通过网线直接一连就可以传的,在这中间会有很多的路由器和交换机,错综复杂的很多设备~~

我们刚才只是考虑了接收方能力有限可能发送丢包,但是中间设备也会接收能力有限,同样会发生丢包,可能数据还没到接收方就先丢了~~
换句话说,最终的传输速度是一个“木桶原理”,取决于最短的一板,要看整个通信链路上哪个设备转发的速度最低,以它为基准来决定发送速度,这个才是合适的~~
我们知道,流量控制是根据接收缓冲区空余空间来定量衡量~~ 现在如果我们考虑这中间的链路又该怎么衡量呢??
这里有多少个设备??不知道
到底哪个设备出现瓶颈??也不知道
所以这里的情况就要比单独的接收方要复杂很多,充满很多变数~~
其实如果不好具体衡量到某个设备,我们就可以把整个通信链路视为“一个整体”,通过”做实验的方式“找到一个合适的窗口大小~~

“面多加水,水多加面”~~
先按照小的窗口(小的速度)先发着~~
如果发的速度很顺利,不丢包,就加大速度~~
一旦出现丢包,就减小速度
又不丢包了,再加大速度
又丢包了,再减小速度
……
就在这个加速和减速的过程中达成一个“动态平衡”,意思是整个窗口大小始终是在改变的,不是一个固定的值,但是这个变化也是在一定范围内的,在这个范围上下去波动,你去算这个平均值可能是一个固定的值,但是实际上具体到某一时刻,这个值都是在实时发生改变的~~
闲聊:这种做实验的思想需要大家重点去体会,毕竟咱们是叫做“工程师”,在解决问题的时候不仅可以理论解决,也可以实践解决,比如之前学过的线程池,线程数要设置多少合适??
不能一概而论~~和机器硬件配置、软件代码、业务场景……都有关系~~
咱们能做到的就是做实验,通过不断调整,选择一个合适的值使资源消耗和产出达到一个你比较满意的状态~~
拥塞控制vs流量控制 哪个说的算??
拥塞控制 和 流量控制 都能限制发送方的窗口大小,就看这两个值,哪个小,哪个就说了算~~
上面是拥塞控制的大体思想,下面来聊聊具体策略~~
下面是 传输轮次 – 拥塞窗口 的关系👇
TCP 引入 慢启动 机制,先发少量的数据,探探路,摸清当前的网络拥堵状态,再决定按照多大的速度传输数据

1.此处引入一个概念程为拥塞窗口(拥塞控制计算出来的窗口大小)
2.发送开始的时候,定义拥塞窗口大小为1
3.每次收到一个 ACK 应答,拥塞窗口加1
4.每次发送数据包的时候,将拥塞窗口和接收端主机反馈的窗口大小做比较,取较小的值作为实际发送的窗口
5.像上面这样的拥塞窗口增长速度,是指数级别的“慢启动”只是指初使时慢,但是增长速度非常快
6.为了不增长的那么快,因此不能使拥塞窗口单纯的加倍
7.此处引入一个叫做慢启动的阈值
8.当拥塞窗口超过这个阈值的时候,不再按照指数方式增长,而是按照线性方式增长
9.当 TCP 开始启动的时候,慢启动阈值等于窗口最大值
10.在每次超时重发的时候,慢启动阈值会变成原来的一半,同时拥塞窗口置回1
少量的丢包,我们仅仅是触发超时重传,大量的丢包,我们就认为网络拥塞

上面的图就表示了拥塞控制的工作过程:
①慢启动
②指数增长
③线性增长
④丢包,窗口变成较小阈值
……
拥塞控制,归根结底是 TCP 协议想尽可能快的把数据传输给对方,但是又要避免给网络造成太大压力的折中方案
闲聊:如果谈过恋爱就能很好理解 TCP 拥塞控制这样的过程~~

所谓爱情,不是风花雪月,而是柴米油盐~~
小结
1.确认应答
2.超时重传
这两个是构成可靠传输的核心部分
3.连接管理
三次握手,四次挥手
4.滑动窗口
批量传输,降低可靠传输带来的损耗
5.流量控制
6.拥塞控制
这两个控制是限制滑动窗口别滑的太快
闲聊:
Q1:啥时候去找实习??
有简历就可以去找实习了,但前提你的简历上要有些项目~~
Q2:git 和 gitee 啥区别??
git 是一个工具
gitee 是一个网站
这两者之间,没啥联系~~
在公司中,一般都是自建 git 仓库服务器的,不会提交到 gitee 上,毕竟没办法保证 gitee 把你的代码泄露出去,训练大模型啥的,所以商业代码托管到 gitee 上是有商业机密泄露风险的
这个自建的过程很简单,就是你有一个服务器,然后在上面搭建一个 git 的服务器的程序就可以了,这个事儿很快,熟悉的话 10分钟 就能搞定
而咱们学生,就没必要弄服务器了,毕竟咱们的代码不值钱~~
咱们就可以把代码托管到 GitHub(全球最大的代码托管网站)
由于国内访问 github 不稳定,才使用 gitee 替代
而 gitee 也不是只支持 git,也支持 svn 等其他版本管理工具
总的来说,这俩之间没啥联系,只不过是咱们目前上传代码的时候是 git 搭配 gitee,以后我们也可能 svn 搭配 gitee,或者 git 搭配 github,或者 git 搭配自建服务器,或者是 svn 搭配 github ……都有可能,都只是把代码托管起来而已~~
Q3:为啥要把代码托管起来??
因为你现在还没遇到硬盘/电脑 坏了的情况……
代码的价值要远远超过你的电脑~~
托管之后,就算换新电脑了,一克隆就能完好如初~~
Q4:为啥这个要墙啊??
很正常,github 被墙
但是让我很震惊的是为啥 of 没有被墙???(最近才解封)
不知道是出了 Bug,还是怎么的~~
网硕互联帮助中心





评论前必须登录!
注册