文章目录
一. 应用层
应用层是和应用程序直接相关, 和程序猿打交道最多的一层
应用层协议, 里面描述的内容, 就是你写的程序, 通过网络具体按照啥样的形式来传输数据
不同的应用程序, 就可以用不同的应用层协议
而实际上, 开发过程中, 很多时候, 都是需要程序猿, 自定制应用层协议的
自定义应用层协议, 本质上就是对传输的数据做出约定:
- 约定传输的数据要有哪些信息
- 传输的数据要遵守啥样的格式
例如, 现在要开发一个外卖软件
信息:
请求: 用户的id, 所在的位置
响应: 商家列表(包括商家名称, 商家照片, 商家评分, 商家的简介)
确定数据的格式:
上述的数据, 都属于"结构化数据"(通过结构体来表示的数据)
而网络中传输的都是二进制字符串, 就需要对数据进行序列化
序列化的方式:
- 基于xml方式
xml是一种经典的数据组织格式
xml是用成对的标签来组织的
xml中, 标签的名字是啥, 标签如何嵌套, 都是可以自定义的
html可以理解成一种特殊的xml, html芮然也是标签结构, 标签有哪些, 每个标签的含义是有一套标准的规范的, 不能自定义出一些自己的标签出来(后来也可以了) - 基于json(当前最流行, 最广泛使用的方式)
可读性很好, 比xml更简洁 - 基于yml
通过缩进来表示 包含 / 嵌套 的关系 - 基于protobuffer(pb)
前面几种都是文本格式, 肉眼能看懂
pb则是二进制格式, 肉眼看不懂
这种方式, 针对要传输的数据进一步整理和压缩了, 虽然可读性不好, 但能够把空间最充分的利用, 最节省网络带宽, 效率也最高
应用层也有一些线程的协议, 供我们直接使用: HTTP协议
二. 传输层
端口号
端口号是一个整数, 2个字节表示的无符号整数 0->65535
小于1024的端口号, 称为:知名端口号, 这些端口号分配给了一些致命的服务器程序, 作为这些服务器的"默认端口号"
1. 同一个机器上, 同一时刻内, 端口号不能重复绑定
查询端口号可以使用: netstat -ano
黄色的部分, 就是端口号, 红色的部分是PID
可以已通过netstat -ano | findstr 9090 , 查看某个端口正在被占用的程序
2. 两个进程, 不能同时绑定同一个端口号, 但是不同的传输层协议可以绑定同一个端口号
如果一个服务器是TCP, 一个是UDP, 此时端口号绑定同一个是可以的
如果两个TCP / 两个UDP, 使用同一个端口, 就会出现绑定失败的情况
** 3. 一个进程, 同一时刻, 可以绑定多个端口号**
传输层的协议
学习一个网络协议, 最主要的就是学习报文格式
UDP
对于UDP来说, 应用层数据到达UDP之后, 就会给应用程序数据包前面拼装上UDP报头
UDP数据报 = UDP报头 + UDP载荷
1. 源端口号
里面记录了程序的源端口号, 2字节
2. 目的端口号
里面记录了数据的目的地的端口号, 2字节
3. UDP长度
描述了整个UDP数据报, 占多少个字节(B)
那么2个字节, 共有16位bit, 能够表示65535(B)这么大的数据, 即能够占用64KB
一个数据报, 最长就是64KB, 不能再长了
如果我们需要传输的数据超过64K, 就需要在应⽤层⼿动的分包, 多次发送, 并在接收端⼿动拼装
如果有面试官问, UDP数据报中, 载荷最多能承载多少数据?
回答64KB, 而不是64KB - 8
4. UDP校验和校验和, 就是数据引入冗余信息, 通过冗余信息来验证原有的数据
数据在网络传输过程中, 时可能会出错的, 例如发生比特翻转, 本来你传输的是0, 实际到了对面变成了1
那么, 就需要能够有办法, 对传输的数据进行校验:
第一层 能够发信是否出错(代价小) ---- CRC
第二层 能发现是哪一位出错, 并且能够进行纠错(代价大) ---- 海明码
在UDP中, 校验和只能够做到第一层, 发现是否有错, UDP就是使用CRC方式完成的校验
CRC是一个简单粗暴地计算校验和的方式, 循环冗余校验
设定两个字节的变量, 把数据每个字节取出来, 往这个变量上进行累加, 如果结果溢出 超过两个字节, 溢出的部分就舍弃, 得到的结果并不重要, 主要是发送方和接收方都通过这种方式进行校验, 那么理论上结果应该是相同的, 这样就认为数据没有问题
除了CRC, 还有一个常见的方法, md5
md5是未来工作中经常用到的方法, 有很多用途
md5的特点:
- 定长
无论输入的内容是多长, 得到的结果, 一定是固定长度 - 分散
输入的内容, 哪怕只改变一点点, 最终结果都会差异很大
因为分散的特性, 因此非常适合做字符串hash算法 - 不可逆
通过原数据, 计算md5, 成本很低, 但是从md5还原成原数据, 成本很高, 仅仅理论上可行
总结: UDP适用于对于性能要求比较高, 但是对于可靠性要求不高的场景
TCP
TCP报头
- 源端口号(2字节)
发送方的端口号 - 目的端口号(2字节)
接收方的端口号 - 序号(4字节)
- 确认序号(4字节)
- 4位首部长度
指的是报头的长度, 不是总TCP报文的长度, TCP总长度从协议的角度没限制
首部长度可以表示的数据范围: 0-15, 但是首部长度的单位是4字节, 所以一共能够表示60个字节
如果首部长度为1111, 那么表示报头有60字节
- 保留(6位)
我们知道UDP的报头长度使用2个字节, 所以报头长度太小了, 但是又不能扩展
保留, 就是在TCP报头中, 提前申请好一块空间, 这个空间暂时不用, 但是说不定未来会用上 - 六位标志位(TCP的灵魂)
- 窗口大小(2字节)
- 校验和(2字节)
- 紧急指针(2字节)
- 选项
选项是optional, 可选的, 可以选择加还是不加
如果选择不加, 选项长度是0, 那么此时报头长度就是20字节(一行4个字节, 一共5行)
如果选项拉满, 此时报头的长度就是60个字节
上述我们没有解释的概念, 会在后面慢慢介绍
TCP协议的核心机制
下面要介绍TCP10个比较核心的机制(并不是只有10个)
** 1. 确认应答机制**
就像发短信, 发送短信后, 对方回复, 接收到的消息, 就叫做"应答", 这样的数据, 称为"应答报文"
应答 => acknowledge => ack
第二个标志位就是应答报文, 如果这个bit位是1, 说明这就是一个应答报文
如果我们发送多条消息, 就可能会发生"后发先至"的情况:
例如:
就容易产生误会
但是, 后发先至, 是网络通信中, 客观存在的, 改变不了
解决办法就是, 对传输的数据, 进行编号 , 并且让应答报文的编号和发送的编号, 能够对应起来
TCP的序号, 是根据字节来编号的, 每个字节都有编号
假设TCP载荷中的数据长度是1000字节, 那么
第一个字节, 编号就是1, 第二个字节, 编号就是2... 每个字节的编号都是连续递增的(不一定是从0或1开始)
此时, 报头中, 写的序号的数值, 就是载荷部分第一个字节的序号 , 因为编号是递增的, 通过第一个字节的序号, 就能知道后续字节的编号
确认序号的取值, 就是要应答数据最后一个字节的序号再 +1
可以理解成:
- 对于B来说, <1001的数据都已经确认收到了
- B再向A索要从1001开始的数据
发送方的数据报, 只有"序号字段"有效, 确认序号无效
接收方返回的sck, 标志位为1, 只有"确认序号"有效, 序号字段无效
2. 超时重传
网络环境, 本身就是不可靠的
数据传输过程中, 被丢弃了, 称为**"丢包"**
每个交换机 / 路由器的转发能力(单位时间转发多少个数据报)是存在上限的
一旦某个设备, 需要转发的数据量超出自身的极限, 此时多出来的部分就可能被直接丢弃掉
丢包是客观存在的随机事件, 是无法预测的
超时重传就是用来应对网络出现丢包情况的策略
正常情况下, TCP是通过 确认应答来知道数据是否被对端收到了
如果A传输的数据出现了丢包, 那么此时B不会收到A的数据, B也不会给出任何应答
那么A就可以根据"是否收到ACK" 来区分是否出现丢包
A从发送出数据之后, 到正常收到ACK, 肯定也会经历一些时间的
A发送完数据后, 就会进行一定的等待, 如果等待时间超过了某个阈值, 还没有收到ACK, 此时就可以任务出现丢包了
如果发现丢包, 就要进行重传
如果是ACK丢了呢?
此时, 站在A的角度, A无法区分是数据丢了, 还是ACK丢了, 所以A做的事情只能是重传
很明显, 此时B就会收到两份重复的数据
此时, B就会针对收到的数据进行去重
接收方, 操作系统内核中, 存在一个数据结构, "数据缓冲区" , 类似于PriorityBlockingQueue
B收到数据后, 层层分用, 分用到TCP这一层的时候, 会有一个阻塞队列 数据结构, 把收到的数据有序的放在这个阻塞队列中
B收到重复的数据不要紧, 我们只需要确保应用层在读取数据的时候, 不要读到重复的数据, 应用层是从这个阻塞队列中的队首读取数据的放的过程中, 就会根据当前数据的序号, 在队列中进行判断, 判定这个数据是否在队列中存在(或者曾经在队列中存在过), 只要存在过, 这个新的数据就不会进入队列, 而是直接丢弃
B如果收到重复的数据, 就可以推断出刚才的ack丢包了, 所以要重新发送ack
网络传输中, 可能会发生后发先至, A按照1 2 3 4的顺序write, 那么B read的时候, 也要按照1 2 3 4read, 虽然传输的过程可能出现后发先至, 是乱序的, 但是不要紧, 在接收缓冲区中, 会对收到的数据先排个序, 让序号小的在前面, 序号大的在后面, 并且数据和数据之间的序号始终都是连续的, 如果前面的数据还没到, 会给这个数据留一个位置, 等待数据过来
超时重传, 超过的时间, 不是固定的数值, 而是会动态变化的
随着重传轮次的增加, 会变得越来越长
假设第一次传输数据, 等待50ms, 就触发重传, 重传之后, 等待100ms, 如果还是收不到ack, 再等待150ms...
超时时间间隔会越来越长, 重传的频率越来越低
重传多次的原因, 大概率是网络出现了严重故障了, 接下来的重传, 能成功的可能性比较低, 如果要是重传的越来越快, 非但得不到好的结果, 而且浪费很多系统资源
如果网络确实出现严重故障了, 重传若干次, 还是不成功, 达到一定次数阈值, 就会尝试**"重置连接"**
触发一个**"重置报文"(RST)**尝试重置连接
第四个标志位, 即RST
重置连接就是通信双方清空之前tcp传输过程中的中间状态, 重新开始传输数据
如果网络出现严重故障, RST报文也无法顺利完成, 此时只能断开连接了(释放掉保存的对端的信息的数据结构)
TCP可靠传输, 是依靠 确认应答 和 超时重传 这两个机制保证的
下篇文章继续介绍TCP核心机制