硅基计划6.0 伍 JavaEE 网络原理

文章目录
- 一、应用层
- 二、传输层——重点
- 1. UDP协议
- 2. TCP协议——重点&难点
- 1. 字节编号
- 2. 可靠性传输
- 3. 超时重传
- 4. 连接管理
- 1. 建立连接
- 2. 断开连接
- 5. TCP状态
- 6. 滑动窗口
- 1. ACK丢失
- 2. 数据包丢失
- 7. 流量控制
- 8. 拥塞控制
- 9. 延迟应答
- 10. 捎带应答
- 11. 面向字节流
- 12. 异常情况
- 1. 进程崩溃
- 2. 主机正常流程关机
- 3. 主机断电式流程关机
- 4. 断网
- 13. 标志位补充
- 三、网络层
- 1. IP协议
- 1. 动态分配
- 2. NAT——重点
- 3. IPV6
- 2. 地址管理
- 3. 路由选择
- 四、数据链路层
- 1. 以太网帧
- 2. MTU
- 五、应用层DNS协议
一、应用层
这一层主要是决定网络传输的数据怎么使用,除了其原有协议,我们程序员还可以去自定义协议,主要有以下几种类别
- XML:这主要表示数据的成对标签,现在很少使用
- JSON:是当下最流行的方案之一,是我们经常用到的一种数据格式
- Gggole Probuffer:其采用了一种二进制表示方式,优势是效率高节省带宽,缺点就是可读性差
二、传输层——重点
注意:我们一下图片中的位指的是比特位
1. UDP协议
我们使用一个简易的模型去描述这个UDP协议的内容,即报头+载荷,本质上是字符串拼接

其中1~4分别是以下含义
- 16位源端口号,我们之前说过端口号最大到
65535,其中1~1023之间的端口号是计算机早期的端口号,现在基本上不用了 - 16位目的端口号
- 16位UDP长度:它表示的是整个报文的长度,是包括报头和载荷的,16个比特位最大情况是
1111 1111 1111 1111即2^16字节≈64kb。因此其所能携带的数据量非常小,超出了这个数据大小就只能被截断,因此这也是UDP的痛点之一,就算你说在应用层方面对UDP传输的数据进行拆包组包,但是你要考虑到网络传输有可能会丢包啊 - 16位UDP校验和:基于
NFS,TFTP,DHCP,BOOTP,DNS等等结合数学公式去计算,数据是否可能会出错
因此综上所述,由于我们说UDP是不可靠的,因此它的适用范围非常有限,既要要求丢包尽可能小,又要要求传输的数据也要尽可能小,因此其适用于机房内的主机之间的数据传输
2. TCP协议——重点&难点
我们同样使用一个比较简单的模型去描述这个TCP协议的内容

我们先来说两个比较基础的
- 4位首部长度:描述的是TCP的报头长度,其4个比特位最大为
1111即60个字节,并且选项的长度必须是4个字节的倍数 - 保留6位:相当于占一个位置,现在还用不到,为了后续扩容使用,从而根本上解决了UDP中无法扩容的问题
1. 字节编号
我们约定字节的编号起始位从某个数开始,一直往后递增
我们的32位序号记录的就是数据部分的第一个字节的编号
同时,32位确认序号指的则是数据部分的最后一个字节的编号+1
2. 可靠性传输
我们可靠性传输靠确认应答机制去保证,就会使用我们上述的字节编号结合确认需要知晓当前的数据是给谁发送的
同时,应答报文是一个特殊的报文,其本身没有载荷,并且在标志位的ACK设为了1
3. 超时重传
如果我们的传输过程产生丢包情况,我们就会设置一个等待时间,过了等待时间后再次重新传输数据,那我们是怎么知道这个数据到底有没有传给对方呢?
我们核心就是使用应答报文机制,我们用一张图描述双方中的一方数据传输异常的两种情况

并且对于接收方,操作系统会给TCP的socket分配一个“接收缓冲区”的内存空间,当我们调用read,scanner等把缓冲区数据读取走(我们之前写的InputStream和OutputStream)。并且如果重传之后发现数据重复,我们会进行去重操作,同时这个接收缓冲区也是类似于消息队列,会根据数据接收的先后顺序对数据进行排序
每一次重新传输的间隔时间会越来越大,避免频繁短时间重传耗费系统资源
如果重传多次都失败,我们一方都会放弃当前连接,把对方的信息从当前主机上删除
TCP可靠传输的核心机制是确认应答机制和超时重传机制
4. 连接管理
通过操作系统内核帮助我们完成建立连接和断开连接两个步骤
我们之前写的socket = new Socket(serverIp,serverPort)就会触发连接操作,当我们的客户端进程结束了,就会触发断开连接的操作
1. 建立连接
我们建立连接的过程是三次握手机制
握手指的是通信是给对方发的一个不带任何载荷的数据包
我们将报文的标志位中SYN设置为了1,即同步报文

Q:为什么要进行三次握手?
- 可以起到“投石问路”的效果,确认双方的通信是否畅通
- 确认双方的发送能力和接收能力是否正常,验证可靠性
- 协商一些特殊要求,诸如
32位符号位的起始位,不能和上一次重复。因为如果上一次连接有丢包的数据,如果我们这一次连接的32位符号起始位还是和上一次一样的话,我们就无法区分这个数据报是这一次连接的还是上一次连接的
2. 断开连接
我们断开连接的过程是四次挥手,同样我们发的报文中6位标志位中的FIN设置为1,即结束报文

为什么是四次握手,不是三次呢,刚刚的建立连接中不是三次握手吗
这其实是因为我们断开连接和关闭连接资源之间不是同步的,因为在关闭连接之前,我们肯定还要做一些其他的收尾工作,而我们的应答报文是接收到客户端发来的断开连接请求就发出响应回去的
5. TCP状态
这里我们以客户端最为主动断开连接的一方
- Listen:服务器特有的状态,在服务器启动的时候且
Socket关联好端口号之后,表示服务器目前处于就绪状态,随时可以进行连接 - Established:服务器和客户端都有,表示连接已完成,可以进行两段通信了
- SYN_SENT&SYN_RCVD:服务器和客户端都有,但持续时间非常短。SYN_SENT表示主动发起SYN那一方的从发出到接收来自对方的ACK之间的状态;同理SYN_RCVD指的是收到对方的SYN到发出ACK之间的状态
- FIN_WAIT1&FIN_WAIT2&LAST_ACK:第一个FIN_WAIT表示客户端发出FIN后且收到服务器的ACK之前;而第二个FIN_WAIT表示的是收到对方的ACK之后到收到对方的FIN之前;而LAST_ACK表示的是客户端发出的回应对方的FIN的最后一次确认应答
- CLOSE_WAIT:断开连接时接收方收到FIN后进入的状态,这个状态一直持续要接收方发出FIN之前(即关闭资源之前)
- TIME_WAIT:表示客户端发出CLOSE_ACK之后会进入的状态,等待几秒,这是为了如果我们客户端发出的LAST_ACK对方没有收到,对方会再次发来一个FIN结束数据报,此时我们可以再次给对方发出LAST_ACK,直到持续到我们TIME_WAITh状态结束对方没有再发出LAST_ACK
6. 滑动窗口
本质上是数据的批量发送和批量等待ACK,提高了数据的传输效率

虽然滑动窗口不不会破坏传输的可靠性,但是如果在批量发送过程中出现丢包呢?我们分为两种情况
窗口的大小最大不一定是64kb,还记得我们的报文结构中有一个选项吧,里面可以规定滑动窗口的大小是16位窗口大小<<窗口扩展因子
1. ACK丢失
这个不用管,假设我们这一次发送的2001~3000数据丢失,但是如果我发到最后发现对方返回的ACK是下一个是5001,那就说明发送方把0~5001的数据已经发送到了,至于对方有没有给我做出回答,我并不关心
2. 数据包丢失
这种情况就比较棘手了,如果我们批量发送了很多数据比如1~1000,1001~2000,2001~3000,3001~4000,而对方一直给我返回的是下一个是1001,一直向我索要1000之后的数据,那次是发送方就明白了,我们1001~2000的数据丢包了

此时我们就要使用快速重传机制,发这个丢失的数据包重新发过去
但是下一次返回的就是下一个是5001,因为我们只是丢了1001~2000之间的数据,其他数据并没有丢,因此对方返回的自然而然就是我们之后的内容啦!!
我们在考虑一种极端情况,如果是传输数据的最后一条ACK丢了,那就会交给我们的超时重传机制去把最后最后的数据再重新试着传输一次
7. 流量控制
这也是为了保证可靠性,我们之前讲过,使用滑动窗口可以采用并发传输数据的方式让数据进行批量传输,但是窗口也不能无限大,如果同时传输的数据量非常大,接收方就会处理不过来,就会产生丢包,下面我们来画一个原理图来简要地讲解下

此时接收方的缓冲区已经满了,如果此时再传输数据进来,就装不下,就会把新传来的数据包丢弃
因此我们流量控制就会依据接收方处理数据的能力反向地去约束发送方滑动窗口的大小
那么这个能力是如何进行衡量的呢?答案就是按照接收缓冲区的剩余空间大小
但是聪明的你又要问了,如果我缓冲区都满了,都不接收发送方的数据了,我发送方怎么知道接收方缓冲区是否已经空闲了呢?
答案就是我们发送方会周期性地给接收方发送一个窗口探测包,触发对方返回ACK,就可以得到新的缓冲区的剩余容量值了
8. 拥塞控制
这个和我们的流量控制类似,都是对我们滑动窗口最初限制,只不过这个是依据通信链路的处理能力,如果中间路劲复杂,我们也很容易产生丢包情况
一旦我们没有发生丢包,我们就开始加大传输速度(即增加窗口大小),一旦发现丢包就减缓速度(即减小窗口大小),因此也就验证了我们窗口的大小是实时变化的
下面我绘制了一张简图用来描述这个过程

- 初始的情况下我们是一个比较小的窗口,进行慢启动,如果后续不丢包,就以指数方式增大窗口
- 达到阈值的时候,我们就进行线性增长,防止由于指数爆炸导致下一次窗口增大就产生丢包
- 当线性增长到一定幅度时,会产生丢包。我们划定新的阈值线,此时我们新版本的做法是线性回落,而老版本是直接跌落,重复上述过程
9. 延迟应答
本质上还是为了提高传输效率,指的就是在有限的机制下,让窗口又尽可能的大
当对方发来传输数据的时候,我们缓冲区当前数据的确很多,但是如果我等一会后,缓冲区数据被消耗了一点,此时我再发回ACK,对方得到的缓冲区大小就会比我最开始直接发ACK得到的缓冲区大小大

但是我还是要强调一点,凡事都没有绝对,万一过了一会接收方缓冲区剩余空间大小没什么变化也不是没有可能
10. 捎带应答
这个会配合延迟应答一起,如果我们的ACK和响应之间的时间间隔很短,那么我们就可以把这两次传输合并成一次,因为你ACK本身并不载荷,结合响应一起发送就能减少每一次发送所消耗的资源,同时也能提升效率
就比如我们之前的三次握手,接收方发送的SYN和ACK大概率就是一起发送的
11. 面向字节流
之前我们就说过这个特性,但是这个特性可能会带来一个问题,即粘包问题
这个主要体现在应用层方面,如果发送方传输了多份数据(比如传输了aa,bbb,cccc),此时接收方接收到的可能是这样的aabbbcccc,就无法区分哪些数据是哪一次发送的
这主要体现在TCP上,而我们UDP发送的是一份份数据包,之间是分开的
对于TCP的粘包问题,我们有两种方案去解决
- 使用特殊字符的分隔符。还记得我们之前写的服务器请求,对于每一次请求,我们都是以**\n** 作为分割条件,这里也是同理,可以使用一些特殊字符
- 我们对每一个数据包开头添加一个固定属性表示数据包的长度,比如
aa就变成[2]aa,bbb就变成[3]bbb,cccc就变成[4]cccc。但是在实际开发中我们基本上不用自己处理,现在的很多框架都能应对这个问题
12. 异常情况
1. 进程崩溃
和我们四次回收相同,当我们进程退出的时候系统会自动释放资源
即使客户端进程消失此时连接还是会存在,系统仍然在等待TCP连接,以便后续四次挥手过程
2. 主机正常流程关机
如果四次回收比较慢,关机比较快,服务器在多次重传FIN后发现客户端还是没有响应,此时服务器就宣告单方面删除客户端信息,即断开连接
3. 主机断电式流程关机
- 如果接收方断电:此时ACK无反应,发送方在超时重传若干次后,放弃连接并且给对方发送一个“复位报文”(对应报头文件的标志位RST设为1)
- 如果是发送方断电:此时接收方不知道发送方式什么情况,会给发送方周期性地发送一个“心跳包”,不断试探触发对方ACK,如果对方无反应就说明对方掉线了
4. 断网
如果是发送方则按照第三种情况的第一种情况处理,如果是接收方则按照第三种情况的第二种情况处理
13. 标志位补充
- URG:仅在紧急指针状态下有效
什么是紧急指针??
本来我们读取数据位是按照序号读取的,但是此时有一个紧急数据要插队即提前读取
但是这种机制现在很少使用了
- PSH:催促标志位,表示让接收方把收到的数据尽快地给到应用程序,但是具体优先级还是要看应用程序是怎么写的
三、网络层
分为地址管理和路由选择
地址管理指的是让网络上每个节点(路由器/主机)描述出位置
路由选择指的是对于双方之间的路径规划
1. IP协议
我们绘制一个IPV4的简图来描述一下

- 4位版本:只有两个取值,一个是IPV4,一个是IPV6
- 4位首部长度:和我们的TCP类似,描述的都是报头的长度,是可变化的,可以和选项结合
- 8位服务类型(TOS):其中3位优先权的字段已经被弃用了,剩下1位保留字段,剩下的4个字段是TOS字段。所谓的TOS字段,之间是互斥的,只能有一位为
1,其余位都要是0,分别表示四种模式最小延迟、最大吞吐量、最高可靠性、最小成本。在SSH/Telent模式下,最小延迟重要;在FTP模式下,最大吞吐量重要 - 16位总长度:即字节数,大小是
64kb,但是如果如果载荷比较大就会自动触发拆包组包操作,具体由以下三个属性决定
- 16位标识:区分出哪些包要组合到一起,后续我们会讲到是由MTU控制的,比如如下内容,它们都具有相同的16位标识

- 13位片偏移:组包时候的排序依据,保证我们TCP是按照正确顺序拆包,正确的顺序组包
- 3位标志位:有一位是保留位,有一位是表示是否拆包,有一位表示这个包的最后一个包(即拆包结束标志,类比我们链表的尾节点的
next域null)
- 8位生存时间:指的就是IP数据包能够在网络上被路由器转发的次数,每次IP数据包经过一个路由器转发,值就减一。通常设置为32/64/128等单位。并且六度空间理论在计算机领域非常适用,因为相邻的服务器之间可以相互感知到其存在
- 8位协议:明确我们在传输层使用什么协议对载荷部分进行解析
- 16位首部校验和:只校验首部,因为我们载荷部分的TCP/UDP已经内置了自己的校验和
- 32位源IP解析:是一个32位数,有四个部分,每个部分采用
.分割,每个部分取值是0~255,下面我们就来重点讨论下这个IP
IP的本质是为了区分不同的设备所代表的一种身份标识,但是我们刚刚说过每个部分取值是0~255总会有上限,因此我们有三个解决措施
我们把IP地址分为两大类,一个是公网IP,一个是私网IP/内网IP(比如诸如10.*,172.16.*~172.31.*,192.168.*)
1. 动态分配
我们在一个设备需要上网的时候再进行分配,不上网的设备暂时先不分配
2. NAT——重点
我们规定在不同的局域网内,内网IP允许重复
但是这也就引申出一个问题,那我如果跨局域网进行访问,对于IP相同的情况我们怎么区分是不同局域网的设备呢
下面我们分几种情况
- 如果是同一个局域网的两台设备相互访问/外网IP的两台设备相互访问,可以
- 如果是不同局域网的两个设备去访问对方,这是不允许的,难道你能借助你家WIFI访问其他人家里的WIFI设备吗
- 外网IP访问内网IP设备,同样不允许
Q:如果是内网IP去访问外网IP设备呢?
答:这就会触发我们NAT的网络地址转换,NAT设备(一般是路由器)会对我们的源IP做出修改,修改为NAT设备自己的IP
这样我们才可以通过外网IP访问外网IP的设备,并且这个NAT设备不是只接收一个设备的访问请求,而是会就收多个请求,这样我们就可以避免大量部署NAT服务器增加成本
但是你是否想过,如果我们的NAT设备接收多台设备的请求,那么我们怎么去区分不同设备的请求呢?
答:可以借助端口号进一步区分,在NAT设备的内部,维护了一张映射关系表,再进行源IP替换的时候就已经记录下来了,等到服务器响应返回的时候,就会根据这张表得知这个响应到底是要发给谁的
即使你的端口号重复了,NAT设备也会自动分配一个新的临时的端口号,即把载荷部分的UDP/TCP的爆头进行修改
这张表的简要结构如下
| 替换前IP | 替换前端口号 | 替换后IP | 替换后端口号 |
|---|

3. IPV6
其报文内容差别比较大,并且使用16字节作为IP地址的表示方式,这相当于给地球上每一粒沙子分配一个IP都绰绰有余
由于西方大国掌握IPV4的IP地址分配权限,我国正在大力推进IPV6,目前已经达到80%普及率,避免西方大国对我国造成威胁
但,IPV6缺点也很明显,和IPV4不兼容,就导致设备更换成本高
2. 地址管理
我们的IP地址被分成两个部分,192.168.100这个部分位网络号,.55这个部分是主机号,并且标准规定
- 同一个网段之间的主机可以有相应的网络号,但是必须有不同的主机号
- 两个相互连通的局域网必须有不同的网络号做区分
对于网络号和主机号,我们可以通过子网掩码配置,在子网掩码的二进制表示中中左侧必须全是1,右侧必须为0,因此你见到的大多数子网掩码是255.255.0.0
上古时期还有一种表示方式即ABCDE类IP地址,但是那样会造成IP地址的大量浪费
并且我们也有三个特殊的IP地址
一个是主机地址二进制位全为0,即192.168.100.0,它表示网络号,任何一台设备都不可以使用这个地址
还有一个是主机地址二进制位全位1,即192.168.100.255,它表示广播地址,是为了获取局域网内所有设备的IP用的
最后一个是以127.*开头的环回地址,这个就是自己访问自己,主要是用在测试自己跟自己通信是否畅通,当然在某些情况下也可以使用localhost来代替
3. 路由选择
这个就是在进行网络传输的路线规划
因为我们IP在传输的时候中间网络的环境是不确定的,因此我们只能靠相邻的路由器之间的相互感知让数据传输走一步看一步,即渐进式/启发式
四、数据链路层
相对于网络层的大的宏观角度,我们数据链路层更加关心**相邻节点(路由器)**之间怎么传输
1. 以太网帧

-
类型:采用十六进制,区分了载荷的不同情况

-
目的地址和源地址都使用MAC(物理)地址表示,并且各个设备的MAC地址在生产的时候已经写死了,不需要NAT去分配

-
CRC:即校验和,用来对数据起到检验的作用
2. MTU
表示的是数据链路层对载荷程度的最大限制,不能超过1500字节≈15kb,如果超过则要进行拆包组包
我们之前说过,IP报头文件中拆包组包并不是通过其最大文件大小是64kb限制的,而是在数据链路层的MTU控制的
因此这样让本就容易丢包的UDP雪上加霜
但是TCP已经考虑到了这一点,内部内置了一个SSM参考值,低于这个参考值就可以避免被拆包
五、应用层DNS协议
全称为域名解析系统,所谓域名就是把IP地址转换成我们人类可以解析读取的字符串单词
早起的时候DNS还是一个HOST文件,每次有新的域名产生就要对HOST文件继续修改,这个只有在自己测试的时候方便,面对每天有大量的新的域名产生和销毁,要进行修改实在太过麻烦
现在我们都是自动记录的,但是如果每次一个设备访问就经过一次DNS服务器,每天承担高并发访问,那服务器不得崩吗
因此在本机对DNS解析后,会对这个DNS域名做一个暂时性的保存,在短时间内访问这个域名不需要再经过DNS服务器解析了
受限于DNS服务器的很多根服务器(数据源)都在西方大国,因此这也是我们大力推动IPV6普及化的又一个重要原因
