文章目录
网络原理
网络初始 , 是对于网络有一个直观的大体的认识
网络编程 , 让我们真正通过代码感受网络通信程序
网络原理 , 进一步的理解网络是如何工作的(理论为主)
应用层
应用层和代码直接相关一层 , 决定了数据要传输什么 , 拿到数据之后如何使用
应用层这里 , 虽然存在一些现有的协议 (HTTP) , 但是也有很多情况 , 需要程序员自定制协议
比如QQ 发送消息 , 构成一个应用层的数据报(举例子)
约定应用层数据报 , 数据格式 , 就是在自定义协议
但是 , 如何约定呢?
-
确定要传输哪些信息 (根据需求走的)
比如叫外卖
请求 : 用户id 用户位置
响应 : 商家信息 : 商家名字 , 商家位置 , 类型
-
确定数据按照啥样的格式来组织 ( 随意约定的 )
网络上传输的 , 本质上都是 0101 , 视为二进制字符串 , 需要把上述这些信息整合成一个字符串
一个简单的方案 , 直接基于分隔符
请求 : 用户 id , 用户位置;
响应 : 商家名字 , 商家位置 , 类型\n
商家名字 , 商家位置 , 类型\n
商家名字 , 商家位置 , 类型;
可以使用任意符号作为分隔符 , 只要分隔符不会在正文中出现即可
属性之间使用分隔 , 整个请求以 ; 结尾
属性之间使用 , 分隔
每个商家使用 \n 分隔
最终结束使用 ; 标识
只要发送方按照这套格式来组织数据
接收方按照这套格式来解析数据 , 两者能对上 , 这样的格式就是可行的
实际开发中 , 有一些现成的格式 , 可以直接使用的
一种典型的格式 : XML
传输层
UDP
UDP的报文格式
2字节表示的范围 0 -> 65535 =>64KB
一个UDP 报文最大长度 , 就是64KB
万一需要传输一个比较大的数据 , 怎么办?
1.可以把一个大的数据拆分成多个部分 , 使用多个 UDP 数据报来传输 , 虽然可行 , 但是比较复杂
2.不用UDP , 直接使用TCP , TCP没有限制
注意: 使用 udp 编程的时候要注意 udp 数据报不能太长, 否则会有问题
校验和
网络传输并非那么稳定 , 可能会出现问题
通过网线传输 , 电信号 , 电信号使用高低电平 表示 01
但是 , 如果外部环境干扰 , 强磁场之类的 , 就可能导致低电平 -> 高电平 高电平 -> 低电平
校验和存在的意义 , 就是用来判定一下 , 当前传输的数据是否出现出错
如果校验和不对 , 此时你的数据一定不对
如果校验和对 , 但是数据也有一定的概率是错的
为了让校验和能够识别率更高一点 , 计算的识货通常会以数据内容作为参数来进行计算 , 数据内筒发生改变 , 校验和也会改变
校验和往往就是去内容 / 内容的一部分 , 通过一些算数运算 , 数学公式变换 , 得到一个数值 , 如果内容发生改变 , 得到的校验和也就发生了改变
发送方 , 把载荷数据带入到校验和算法中, 计算生成得到的校验和结果 , 设置为sum1
发送方就把这一整串数据发送给接收方了
接收方收到的数据 , 既有载荷 , 也有校验和sum1
接收方就可以把载荷按照同样的算法 , 再计算一遍校验和 , 得到了sum2
对比 sum1 和 sum2 是否相同 , 如果不相同 , 数据一定出问题了
前提 : 输入的内容一样 , 按照一个算法得到的校验和 , 结果也是一样的
TCP
-
有连接
Socket clientSocket = serverSocket.accept(); socket = new Socket(serverIp, port);
-
可靠传输
TCP存在的初心 , 最核心的机制
-
面向字节流
-
全双工
try (InputStream inputStream = clientSocket.getInputStream(); OutputStream outputStream = clientSocket.getOutputStream()) { }
确认应答机制
实现可靠性的最核心机制
举一个生活中的小例子~~
你和妈妈在家 , 你想让妈妈做饭吃
后发先至情况:
先收到了 “自己做去” , 后收到 “好啊好啊”
为了解决上述问题 , 就需要针对信息进行编号 , 给发送的消息分配一个 “序号” , 同时应答报文 , “给出确认序号”
真实的 TCP 数据传输也是引入了 序号 和 确认序号
TCP 是针对每个字节都去编号 , 从前往后 , 把每个字节分别分配一个编号
注意:
确认序号的规则:
不是发送方发过来的序号是啥 , 确认序号就是啥
而是取的是发送方发过来的所有数据 , 最后一个字节的下一个字节的序号
确认序号 1001 的含义:
1.小于1001 的数据 , 我已经收到
2.我接下来想向发送方索要从 1001 开始的数据
接收方就可以通过 ack 的确认序号 , 告诉发送方哪些数据已经收到了
TCP 会有一个接收缓存区 (一块内核中的内存空间 )
每个socket 都有一份自己的缓冲区
如果一切顺利 , 就可以直接确认应答了 , 可靠性自然得到了支持
超时重传
为什么会丢包 :
如果中间任何一个节点 , 出现了问题 , 都可能导致丢包
每个设备 , 都是在承担很多的转发任务的 , 且转发能力都是有上限的
某一个时刻 , 某个设备 , 上面的流量达到峰值 , 就可能会引起部分数据被丢包
如果包丢了 , 接收方就收不到了 , 自然就不会返回 ack
发送方就迟迟拿不到应答报文 , 等待一段时间之后 , 还是没有收到 应答报文 , 发送方就视为刚才的数据丢包了 , 就会重新发一遍
发送方对于丢包的判定 , 是一定时间内 , 没有收到 ack
1.数据直接丢了 , 接收方没有收到 , 自然不会发ack
2.接收方收到数据了 , 返回的 ack 丢了
发送方式区分不了这两种情况 , 只能都重传
这种情况下 , 主机B会收到重复的数据 , TCP 已经处理好此类问题 , 会在接收方缓冲区中根据收到的数据的序号 , 自动去重 , 保证了应用程序读到的数据仍然只有一份 .
那重传后的数据有没有可能又丢了呢
?
也是有可能的 , 一旦出现连续丢包 , 很可能网络出现了问题
``TCP针对于多个包丢失 , 处理的思路是 , 继续超时重传`
但是每丢包一次 , 超时等待时间都会变长 , (重传的频率降低了) , TCP觉得重传也没有用处(网络出现大问题)
连续多次重传 , 都无法得到 ACK , 此时TCP就会尝试重置连接(相当于充实重连) , 如果重置连接也失效 , TCP 就会关闭连接 , 放弃网络通信了)
一切顺利 ,使用确认应答保证可靠性
出现丢包 , 使用超时重传作为补充 ~~~这两个机制 是TCP 可靠性的基石
连接管理
TCP建立连接 : 三次握手
握手 指的是通信双方 , 进行一次网络交互 , 相当于 客户端 , 服务器 之间 , 通过三次交互 , 建立了连接关系
上述过程内核自动完成, 应用程序干预不了 , 等到连接完成了 , 服务器 accept 把建立好的连接从内核中拿到应用程序中
syn : 称为 同步报文段
意思就是一方要向另一方 , 申请建立连接
什么叫做 syn 报文(观察TCP 报头结构)
这 6 个特殊的比特位 , 这几位默认是 0
如果设为 1 , 则表示特定含义 .
其中第二位 , 是ACK , 如果这一位是1 , 表示当前TCP 数据报是一个应答报文
其中第五位, 是SYN , 如果这一位是1 ,表示当前TCP数据报是一个同步报文
如果一个TCP 数据报, 第二位和第五位都是1 , 则当前这个报文 是 SYN + ACK
**三次握手这个过程 , 本质上是投石问路 , 验证了客户端 和 服务器 , 各自发送能力和接收能力是否正常.**确认了客户端和服务器 各自的发送能力和接收能力 都正常 , 这就是后续可靠传输的基础
两次握手行不行 ?
不行 , 服务器不能确定客户端是否能接收到消息
四次握手行不行 ?
把中间的 syn 和 ack 拆开分别发送 , 可以但没必要
分成两次发送 , 效率不如合并成一次
TCP断开连接 : 四次挥手
通信双方 , 各自给对方发送一个 FIN(结束报文) , 再各自给对方返回ACK
建立连接 , 一定是客户端主动先发起
断开连接 , 客户端和服务器都有可能先发起
ACK 和 FIN 能合并吗
有一定概率合并成一个 , 但是通常情况下 , 不能合并的
为啥什么三次握手 的syn和ack能合并, 而四次挥手不能?
三次握手 : ack 和 syn 是同一个时机触发的 (都是内核来完成的)
四次挥手 : ack 和 fin 则是不同时机触发的
ack 是内核完成的 ,会在收到 fin 的时候第一时间返回
fin 则是应用程序代码控制的 , 在 调用到 socket 的 close 方法的时候才会触发fin
滑动窗口
提高可靠性 , 往往意味着损失效率
此时 主机A 这边就花费了大量的时间在等待ACK , 想要提高效率 , 就需要缩短等待时间 , 批量发送数据 , 一次发多条数据 , 一次等多个ack
这里就是批量发送 4 条数据 , 发完之后 , 同一等待 ack , 每次收到一个 ack 就立即发下一条 , 使用一份时间 , 等待多个 ack , 总的等待时间缩短了 , 整体的效率就提升了
上述批量传输数据的过程, 称为滑动窗口
批量不是无限发送 , 是发送到一定程度 , 就等待 ack , 不等待直接发送的数据量是有限的 , 每回来一个 ack 就立即发送下一条 , 相当于总的要批量等待的数据是一致的. (把批量等待数据的数量 , 就称为 “窗口大小”)
批量发送了四条数据 , 就等待着四个ack , 白色区域 , 就相当于等待的窗口
当收到 2001 这个 ack 意味着 1001-2000 这个数据得到了确认 , 此时就会立即发下一个5001-6000这个数据
此时看到的效果 , 就好像窗口还是这么大 , 但是往后挪了一个格子 , 如果收到 的 ack 非常快 , 此时这个窗口就在快速的往后滑动
- 批量发送的过程中 , 如果出现丢包怎么办?
这个图中 , 相当于一半的 ack 都丢了 , 相当高的丢包率
注意 : 这种情况对于可靠性没有任何影响
确认 序号的含义 , 表示 , 该序列号之前的数据都已经收到了 , 后一个 ack 能够涵盖前一个 ack 的意思
当收到2001 这个 ack 的时候 , 此时发送方就知道了 , 2001 之前的数据都收到了 ,1-1000 这个数据也收到了 , 1001 这个 ack 丢了就丢了 , 不影响
如果是最后一个丢了 , 照常超时重传 ,滑动窗口是锦上添花
这就是确认序列号的目的
由于1001-2000 这个数据丢了 , 所以接收方仍然继续索要1001 , 不会因为收到的是2001-3000 就返回3001
接下来几次的数据的 ack ,确认序号都是 1001 , 主机B 再向主机A反复索要 1001这个数据 , 主机A 这边连续收到几个 1001 之后 ,就知道 需要重传 1001-2000这个数据
当主机A把 1001-2000 这个数据重传 , 主机 B收到之后 , 返回的 ack 确认序号是7001 , 而不是2001 , 因为 2001-7000 这些数据 , 主机B都已经都到过了
上述重传过程中 , 没有任何冗余的操作 , 丢了数据才会重传 , 不丢数据不必重传 , 整体速度是比较快的 , 这个重传过程也称为快速重传
滑动窗口 , 快速重传 , 是在批量传输大量数据的时候 , 会采取的措施 , 如果你就只传输一条两条 , 少量的,低频的操作 , 就不会按滑动窗口这么执行, 仍然是前面的确认应答和超时重传
流量控制
也是保证可靠性的机制
滑动窗口 , 批量发送 , 窗口越大, 相当于批量的数据越多 , 整体的速度就越快
如果发送的太快 , 瞬间把接收方的接收缓冲区给打满了 , 接下来继续发送 , 此时数据就会丢包 ,这种情况得不偿失
通过流量控制 , 本质上就是让接收方 , 来限制一下发送方的速度
具体是如何控制的呢?
让ack 报文中 ,携带一个 “窗口大小” 这样的字段
当 ack 为 1 的时候 , ack 报文此时窗口大小字段就会生效 , 这里的值就是建议发送方的窗口大小 . 接收方直接拿接收缓存区的剩余空间作为窗口大小
拥塞控制
滑动窗口的大小取决于流量控制(衡量了接收方的处理能力)和拥塞控制(衡量了传输路径的处理能力)
例如 :主机A和主机B进行通信
很明显 , 传输路径上 , 任何一个设备 , 处理能力如果遇到瓶颈都会对整体的传输速率产生明显的影响.
拥塞控制 做的事情 , 就是衡量中间节点 传输的能力
(拥塞控制 , 是要衡量中间路径 . 中间路径上有多少个节点 , 每个节点当前的情况 , 每次传输所有的路径不同)
通过实验的方式 , 找到一个合适的发送速率
开始的时候 , 按照一个小的速率发送 ,
如果不丢包 , 就可以提高一下速率(扩大窗口的大小)
如果出现丢包 , 则立即把速率调小
重复上述过程
有两个窗口 : 拥塞窗口(拥塞控制试验出来的窗口) , 流量控制的窗口
实际发送方的窗口大小 = min(拥塞窗口 , 流控窗口)
延迟应答
提高传输效率!!!
TCP 可靠性的核心是确认应答 .
ACK 要发 , 但是不是立即发 , 而是稍微摩擦一会再发
TCP 中决定传输效率的关键元素就是 窗口大小
延迟应答的效果 , 就是通过这个延迟 , 让接收方应用程序 , 趁机多消费点数据 , 此时反馈的 窗口大小 , 就会更大一丢丢 , 此时发送方的发送速率也就能快一点 , (同时也能满足让接收方能够处理过来)`
捎带应答
基于延迟应答
客户端服务器 通信模式:
- 一问一答 . 绝大部分服务器都是这样的
- 多问一答 . 上传大文件
- 一问多答 . 下载大文件
- 多问多答 . 游戏串流
客户端服务器 , 之间的通信模式 , 通常是 “一问一答” 这种模式的
为什么四次挥手 , 有可能是三次挥完 . 捎带应答起到的效果 . 因为有可能 ack延迟应答 , 和fin同时了
面向字节流
举个例子 粘包问题
主机A的视角 : 主机B说的话从哪里到哪里
主机B的视角 : 主机A说的话从哪里到哪里
粘包问题:
所谓 “一句话” 就相当于一个 “应用层数据报”
当 A 给 B连续发送了多个应用层数据报之后 , 这些数据就都积累到 B 的接受缓冲区中 , 仅仅挨在一起 , 此时 B 的应用程序在读数据的时候 , 就难以区分从哪到哪是一个完整的应用层数据报 , 就很容易读出 半个包 / 一个半包
解决上述问题有两个方案 :
1.定义分隔符 , 粘包问题有效解决方案
2.约定长度 , 约定数据的前4 个字节 , 表示整个数据报的长度
这两个都是 自定义应用层协议 的注意事项 !(像 xml , json ,本质上都是通过分隔符 的方式来实现)
异常情况
1.进程关闭 / 进程崩溃
进程没了 , socket 是文件 , 随之被关闭 , 虽然进程没了 , 但是连接还在 , 仍然可以继续四次挥手
2.主机关机 (正常流程关机)
先杀死所有的用户进程
也会触发四次挥手 , 如果挥完 , 更好. 如果没挥完 , 比如 , 对方发的 fin 过来了 , 咱们没来得及 ack 就关机了 , 此时对端就会重传 fin , 重传几次后 , 发现没有 ack , 尝试重置连接 , 如果还不行 , 就直接释放连接
3.主机掉电 (拔电源)
瞬间主机关机 , 来不及进行任何挥手操作
- 对端是发送方 : 对端就会收不到 ack => 超时重传 =>重置连接=>释放连接
2)对端是接收方: 对端是没法立即知道 , 你这边还没来得及发新的数据 , 还是直接没了 , TCP 内置了 心跳包 , 保活机制
心跳包
a)周期性
b)如果心跳没了 , 挂了
虽然对端是接收方 , 对端会定期给咱们发一个心跳包 (ping) , 咱们返回一个 (pong)
如果每个 ping 都有及时的 pong , 这个时候说明当前对端的状态良好 , 如果 ping 过去之后 , 没用pong , 说明心跳没了
4.网线断开
同上
TCP小结
TCP 和 UDP 应用场景的差别:
TCP 可靠传输 , 效率没那么高 , 绝大部分场景下 , 都可以使用 TCP
UDP不可靠传输 , 效率高 , 对于效率要求较高 , 对于可靠性不高的情况下
网络层
地址管理
每个网络上的设备 , 要能分配一个地址 (唯一)
IP 地址 , 本质上就是一个 32位 的整数
通常 , 会把 32 位 的整数 , 转换成点分十进制的表达方式 , 三个点 , 把这个整数分成 4 个部分 , 每个部分 , 一个字节 , 每个部分的取值范围 0-255
32位 的整数 , 最多能表示多少个不同的地址 ? 42亿9千万(不够用)
如何解决上述问题 ?
1.动态分配IP 地址 : 设备上网才分配 , 不上网就不分配 , 此时就可以省下一大批IP地址 , 但是并没有增加 IP的数量 , 只能一定程度的缓解 , 不能彻底解决问题
2.NAT 机制
把所有的IP地址分成两大类
内网IP : 10.* 172.16.* -172.31.* 192.168.*
外网IP : 剩下的IP
外网 IP 必须是唯一的
内网 IP 则可以重复出现 ,(尤其是在不同的局域网中)
内网设备如果要访问外网 , 会给他分配一个外网IP
但是这个外网IP 不是这个设备独占的 , 而是这个内网中所有的设备都公用这一个外网IP了
很明显 , 百度的响应是不同的
怎么确定不同响应返回给主机是对应的?: 路由器会处理
3.IPV6
IPV4 是传统的 IP 协议 , 使用 4 个字节 , 32 位来表示 ip地址 2`32
IPV6 是更新一些的 IP 协议 , 使用16 个字节 , 128 位来表示 ip 地址 2`128 (地球上的没一粒沙子分配一个ip都是够用的)
IPV6 和 IPV4 并不兼容
一个普通的 IPV4 路由器 , 要想支持 NAT , 软件上升级下系统就行了
一个普通的 IPV4 路由器 , 要想支持 IPV6 , 光升级软件没有用 , 还得升级硬件
IP 地址的组成:
IP 地址分为两个部分 , 网络号和主机号
- 网络号 : 标识网络 . 保证相互连接的两个网段具有不同的标识 (表示一个局域网)
- 主机号 : 标识主机 . 同一网段内 , 主机之间具有相同的网络号 , 但是必须要有不同的主机号(标识一个局域网内部的主机)
对于网络号主机号的划分 , 主要有两种分类方式 :
1.IP地址分类 (ABCDE)
前缀用来区分类别 . 每个类别下 , 网络号和主机号长度都是固定的
2.子网掩码
特殊的 IP 地址 :
主机号为 0 的 ip : 192.168.0.0 就是网络号 , 局域网里不应该存在某个主机 , 主机号是0
主机号全为1 : 192.168.0.255 广播地址 , 往这个地址上发送 udp 数据报 , 此时这个数据报就会被转发给整个局域网中的所有主机
IP 为 127 开头的 : 127.* 称为环回IP 环回IP 对应特殊的虚拟网卡 lo. 通过环回 ip 传输的数据 , 走这个虚拟网卡 (这个过程没有 IO 操作的 , 纯内存操作的) , 要比一般的这种普通ip 的数据传输要快
主机号为1 : 192.168.0.1 一般作为"网关ip" ,
路由选择
路由选择做的事情 : 从主机A =>主机B , 之间的具体路线怎么走 .
网络上 , 环境复杂 , 某个路由器是无法把整个网络环境都记录下来的 , 路由器只能记录周围的情况 ( 路由器内部使用的 路由表 这样的数据结构来记录邻居信息 ) , 实际转发过程中 , 是渐进式的 .
IP 数据报 , 在进行网络转发的过程中 , 就是一个 “逐渐问路” 的过程 , 每个路由器只能认识周围的情况 , 很可能问的目标并不知道(目的ip 在路由表汇总 , 没有匹配的结果) , 此时就会走路由器给你指出的一条默认路径 ( 路由表中的 “下一跳表项”)
数据链路层
更接近底层了 , 距离程序员更远
以太网 : 数据链路层 / 物理层
以外网数据帧
把这个数据链路层数据帧 , 最大载荷长度 , 称为 MTU
如果载荷的数据 , 长度超过 MTU , 就会在 IP 层进行分包 , 使每个分出来的结果 , 都能在 MTU 之内 .
虽然 MTU 有限制 , 但是没关系 , IP仍然可以保证传输一个更大的数据
虽然 IP 能拆包 吗仍然不能改变 UDP 最大长度是64k 的事实 , 由于拆出的这些 IP数据报中只有一份 UDP 首部 , 这个首部能够填写UDP长度的地方 , 也还是只有 2 个字节 , 64 k 这个限制还是存在的