传输层网络协议
1. UDP协议
1.1 特点
- 面向数据报(DatagramSocket)
- 数据报大小限制为64k
- 全双工
- 不可靠传输
- 有接收缓冲区,无发送缓冲区
UDP的特点,我理解起来就是工人组成的**“人工传送带”**:
- 面向数据报(DatagramSocket):工人们能够知道的只有装货物的袋子,并不知道里面装的什么内容,这个麻袋上印着各种信息,包括送往哪里,送给谁,谁发送的等能够确保货物定位到人的信息。
- **数据报大小限制为64k:**一个麻袋装的货物是有容量限制的。
- **全双工:**这条“人工传送带”可以将货物正向运输,也可以逆向运输。
- **不可靠传输:**工人只管将一袋一袋的货物运到目的地,但是不管袋子中的货物是什么,也不管发货地与收货地的老板沟通好了没有,工人们收到的指令只是将货物运到从起始地运到目的地。
- **有接收缓冲区,无发送缓冲区:**工人们将货物运走的过程中,起始地并没有设置缓冲区,工人可以直接搬走,但是目的地有缓冲区,即目的地是有放置一部分货物的地方的。
1.2 UDP协议端格式
1) 源端口
表明数据从哪里来
一般是固定的, 就像每天饭店基本都是一个位置进行接客.
2) 目的端口
表明数据发往哪里
一般是不固定的, 就像来饭店吃饭的客人, 每天来这个饭店吃饭的位置是不固定的, 再说了, 可能昨天坐的位置今天已经坐上了其他人.
3) UDP报文长度
表明当前的报文有多长.
取值范围为 8 ~ 65535 字节
4) UDP校验和
表明当前数据是否正确, 在UDP中一般是使用较为简单的校验方式.
可能的方式:
- 将所有的字节进行相加
- md5
- CRC算法
- 循环冗余校验
1.3 模型
以图示进行说明:
从上图中也可以看出来:
- UDP并没有所谓的建立连接的过程,只是需要发送数据的时候就去调用socket.send()进行发送。
- 一共经历了两次send()和receive()操作
1.4 代码
完成的是一个"回显"的UDP代码。
1) 客户端代码
package UDP;
import java.io.IOException;
import java.net.*;
public class Server {
private DatagramSocket serverSocket = null;
public Server(int port) throws SocketException {
serverSocket = new DatagramSocket(port);// 服务器端需要指定端口
}
public void start() throws IOException {
while (true) {
System.out.println("等待客户端连接...");
// 1. 构造数据报
byte[] bytes = new byte[1024];// UDP接收信息不可超出 64*1024b
DatagramPacket reqPacket = new DatagramPacket(bytes, bytes.length);
// 2. 接收数据报并解析
serverSocket.receive(reqPacket);// 输出型参数
String req = new String(bytes, 0, reqPacket.getLength());
System.out.println("收到服务器请求: " + req);
// 3. 构造响应并发送数据报
String resp = process(req);
DatagramPacket respPacket = new DatagramPacket(resp.getBytes(), 0, resp.getBytes().length, reqPacket.getSocketAddress());
serverSocket.send(respPacket);
System.out.println("已反馈: " + resp);
}
}
private String process(String req) {
return req;
}
public static void main(String[] args) throws IOException {
Server server = new Server(2024);
server.start();
}
}
2) 服务端代码
package UDP;
import java.io.IOException;
import java.net.DatagramPacket;
import java.net.DatagramSocket;
import java.net.InetSocketAddress;
import java.net.SocketException;
import java.util.Scanner;
public class Client {
private DatagramSocket clientSocket = null;
public Client() throws SocketException {
clientSocket = new DatagramSocket();// 客户端是随机端口
}
public void start() throws IOException {
while (true) {
System.out.print("->");
// 1. 构造请求并发送
Scanner scanner = new Scanner(System.in);
String req = scanner.next();
DatagramPacket reqSocket = new DatagramPacket(req.getBytes(), 0, req.getBytes().length, new InetSocketAddress("127.0.0.1", 2024));
clientSocket.send(reqSocket);
System.out.println("已发送: " + req);
// 2. 接收服务器请求
byte[] bytes = new byte[1024];
DatagramPacket respPacket = new DatagramPacket(bytes, 0, bytes.length);
clientSocket.receive(respPacket);
String resp = new String(respPacket.getData(), 0, respPacket.getLength());
System.out.println("收到服务器反馈: " + resp);
}
}
public static void main(String[] args) throws IOException {
Client client = new Client();
client.start();
}
}
运行结果:
3) 注意事项:
-
发送的时候必须在DatagramPacket中指定目的地,即:
-
客户端:
new InetSocketAddress("127.0.0.1", 2024)
客户端是因为是本次通信中需要主动建立连接的一方,所以由客户端进行提出连接到哪台服务器和哪个端口对其进行提供服务。
-
服务端:
reqPacket.getSocketAddress()
服务端提供服务,在向客户端反馈响应的时候,这个地址应遵循:“谁发送过来,就返回到谁那里”。
-
-
接收数据报的时候并不需要指定任何地址或者端口。
-
UDP接收信息不可超出 64*1024b
-
数据报中包含了一切所需要的信息,Datagram’Socket只是辅助完成发送和接收的操作。
2. TCP协议
全称为传输控制协议(Transmission Control Protocol).
2.1 特点
- 面向字节流
- 无数据报大小限制
- 全双工
- 可靠传输
- 有接收缓冲区,有发送缓冲区
2.2 TCP协议端格式
1) 源端口号\目的端口号
同UDP, 表明数据从哪来, 到哪去
2) 序号\确认序号
表明当前数据传输到哪一位
会通过序号控制当前发送的数据到了哪一位了
3) 首部长度
表明了当前的TCP的报头的长度, 因为TCP的报头长度有固定的20字节, 其余还有选项部分, 可将TCP的报头最大扩充到60字节
- 20字节: 2+2+4+4+2 (4bit+6bit+6bit) +2+2+2 = 20字节
- 因为首部长度只有4位, 但是报头的长度范围在20~60之间, 所以想要使用4位二进制表示60位, 单位就是4字节, 而不是1字节, 如此一来, 15*4 = 60.
4) 保留位
为了将来扩充TCP报文使用
5) 6位标志位
-
URG: 紧急指针是否有效
用于标记优先发送的数据
-
ACK: 确认信号报文段
用于 “确认应答” 机制中重要报文, 表明ACK发送方已经接收到了当前发送过的报文
-
PSH: push, 提示消息接收方应当立即将数据传送到应用, 而不是留在缓冲区
-
RST: reset, 复位报文段, 表明当前连接出现严重差错, 需要进行释放后重新连接.
用于拒绝非法报文段, 并非直接重新进行连接
-
SYN: synchronization, 同步报文段, 表明发送方想要建立连接
用于 “三次握手” 机制中:
- 客户端向服务器发送SYN报文, 表明想要同服务器进行连接
- 服务器与此同时反馈 SYN 与 ACK 报文, 表明收到客户端的请求连接, 并返回同意建立连接报文
- 客户端发送 ACK 报文, 表明客户端收到服务器同意建立连接的反馈
-
FIN: finish, 结束报文段, 表明当前报文传输完毕
用在 “四次挥手” 机制中:(假设客户端想要主动进行关闭连接)
- 客户端发送 FIN 报文, 表明想要关闭连接, 客户端进入 FIN_WAIT1 状态
- 服务器收到 FIN 后, 向客户端发送 ACK 报文告知客户端, 表明收到关闭请求, 服务器进入CLOSE_WAIT 状态, 客户端进入 FIN_WAIT2 状态
- 服务器发送 FIN 报文, 表明即将关闭同客户端的连接, 服务器进入 LATST_ACK 状态, 客户端进入 TIME_WAIT 状态
- 客户端发送 ACK 报文, 表明客户端收到服务器的关闭指令, 服务器进入 CLOSED 状态
- 客户端一段时间后仍然未收到任何错误信息, 也进入CLOSED状态
2.2 TCP原理
1) 确认应答
一共有两点注意事项:
- TCP对于数据报中的每一个字节都进行了标号, 确保数据接收方收到无序的数据仍能组合为有序数据.
- 当接收方收到数据时, 会发送ACK报文表明自己收到了数据, 同时这个报文中会携带着确认序号, 这个序号一共有两层含义:
- 表示自己已经收到了数据
- 表示下一个应该收到的数据报是从第几个序号开始
2) 超时重传
超时重传会发生在双方无法正常进行数据传送的时候, 主要有两种情况:
-
发送方的数据报丢包
此时发送方在一段时间后并未收到接收方的ACK报文, 发送方就知道自己的数据并没有传送到对方, 所以就会再次传送, 收到ACK报文后停止重传,.
-
接收方的ACK报文丢包
接收方发送的报文没有成功发送, 就会使得数据发送方以为数据并没有成功传输, 也会触发超时重传, 这样就会使得接收方收到重复的数据, 于是, 接收方通过序号这一报文结构, 就可以丢弃重复的数据.
超时的时间怎么确定?
超时的时间会呈现越来越长的特性, 因为一次重传不成功, 再重传说明可能是路径的问题, 但是两次三次不成功大概率是本来就传不过去, 所以需要指数形式递增
3) 连接管理 (安全连接机制)
分为三次握手, 四次挥手
三次握手
- 客户端发送 SYN 报文, 请求建立连接
- 服务器返回 ACK+SYN 报文, 表明收到报文, 并同意建立连接请求
- 客户端返回 ACK 报文, 表明收到服务器的同意报文
四次挥手
-
客户端发送FIN报文, 进入 FIN_WAIT1 状态
-
服务器收到后, 返回ACK确认报文, 表明收到客户端想要关闭连接的请求, 同时服务器进入 CLOSRED_WAIT 状态
-
随即服务器发送FIN报文, 表明它也准备关闭连接, 同时进入 FIN_WAIT_1 状态
-
客户端收到服务器的 FIN 报文后, 发送 ACK 确认报文, 进入 TIME_WAIT_2 状态
这个状态的存在是为了能够保证安全关闭, 如果不进入 TIME_WAIT 状态, 直接在发送完最后一次 ACK 报文后直接关闭连接, 那么如果产生意外情况(ACK报文丢包等), 就会产生异常
-
服务器收到客户端的报文后, 进入 CLOSED 状态
-
一段时间后, 客户端进入 CLOSED 状态
这个时间是 2MSL(Max Segment Life, 报文最大生存时间), 这个同超时重传的设定类似:
- 一是能够保证未传输完毕的数据全部传输完毕再关闭
- 二是能够保证最后一个 ACK 确认报文发送完毕 (就算 ACK 报文丢包, 但是此时 TCP 连接尚未断开, 所以还是能够触发超时重传)
其他状态解释
- CLOSE_WAIT 是指发送 FIN 报文达到的状态
- TIME_WAIT 是指接收到 FIN 报文达到的状态
一般出现大量的 TIME_WAIT, 就意味着没有正确关闭, 加上关闭的代码即可.
- LISTEN 是指服务器的ServerSocket已经创建好, 并且绑定端口完成, 就等客户端发送数据, 是 “监听” 状态
- ESTABLISHED 是指客户端与服务器已经建立好连接, 是 “确立完成状态”
4) 滑动窗口 (效率机制)
一问一答(一个数据, 一个 ACK 确认报文) 的方式效率较为低下, 所以衍生出了 滑动窗口
窗口是当前能够一次发送最大的数据报的数目, 能够使得接收方不必一对一进行接收, 而是一个 ACK 可以表示收到多个数据报.
过程
假设滑动窗口的大小就是5个数据报.
-
发送前5个数据报都不需要服务器反馈 ACK确认报文, 直接发送
-
但是此时服务器会阻塞, 直到服务器返回第一个数据报的确认报文(ACK 携带着第二个报文的序号, 但是第二个报文已经发送完毕), 第六个数据报才会进行发送
第七 \ 第八个报文都是在收到第二 \ 三个报文的时候才会进行发送,并且不会要求立马收到 第七 \ 八个报文的 ACK, 而是等待收到第二 \ 三 个报文的确认报文
数据发送乱序怎么办?
通过序号可以对于数据进行重新排序
数据发送出现 “后发先至” / “先发后至” 怎么办?
通过确认序号, 大的序号可以覆盖确认小的数据报已经收到的消息.
就像别人问你, 你参加过高考吗, 你回答我已经本科毕业了, 这个本科毕业就是对于高考这个事件的覆盖性回应, 既然本科都已经毕业了, 那么高考就一定已经参加过了 (对于一般人的学习历程来说)
过程中出现丢包怎么办?
分为两种情况:
-
一是数据报已经到达, 但是接收方的 ACK 确认报文丢包
这种情况并不需要进行特殊处理, 随后的确认报文会进行 “覆盖确认”
- 比如 前1000字节的ACK确认报文丢包, 但是后来的第 3000 字节开始的数据报的 ACK 报文收到
- 这时也就可以确认前 1000 字节的 ACK确认报文已经收到
-
二是发送方的数据报丢包
此时接收方在未收到本应收到的数据报后, 一直重复发送确认序号进行索要应当发送的数据报
- 发送方: 发送1000-2000 (丢包)
- 接收方: 返回确认序号: 1001
- 发送方: 发送2000-3000 (正常)
- 接收方: 返回确认序号: 1001
- 发送方: 发送3000-4000 (丢包)
- 接收方: 返回确认序号: 1001
- 发送方: 发送4000-5000 (正常)
- 接收方: 返回确认序号: 1001
- 发送方: 发送4000-5000 (正常)
- 接收方: 返回确认序号: 1001
就像, 不耐烦地和商家商量退款操作, 不管商家给你解释什么理由, 你总是重复: “退款”
于是, 商家在收到你的三次重复 “退款” 请求之后, 进行了退款, 也即 TCP 中的重传操作.
这一过程称之为==“高速重发机制”== (也叫 “快重传”)
注意事项
-
值得一提的是, 一个滑动窗口需要使用缓冲区对于已经传送到的数据进行记录, 如果没有传输完成, 且已经达到应该传送完毕的状态, 那么就在下一次的确认报文中一直附带这个序号, 一直向发送方进行索要数据报.
- 缓冲区接收数据是从小的序号开始接收, 这一特性是根据优先级队列实现的
- 缓冲区中的数据如果没有被接收端的应用程序读取, 那么就会一直留在缓冲区, 留下来的作用就是去重.
-
滑动窗口的实际大小不仅仅是 16位窗口大小, 还需要乘以窗口扩展因子
- 窗口大小 * 2^窗口扩展因子
5) 流量控制 (安全机制)
这个机制就像小学的奥数题:
游泳池一边放水, 一边蓄水, 问你什么时候才能将水池的水放满?
这里的放水就相当于处理数据报, 蓄水相当于发送数据报, 只是我们的目标是使得 TCP 缓冲区这个 “游泳池” 不满, 所以需要进行控制 “蓄水” 的速度, 以匹配 “放水” 的速度.
因此, 根据接收端的数据处理能力, 进行控制发送端的数据发送速度的机制, 就叫做 “流量控制”.
过程
-
发送方不停发送数据报, 使得缓冲区的可用空间持续减少
-
每次缓冲区减少的时候, 都通过 ACK 确认报文携带剩余空间进行发送
这条数据是放在 “窗口大小” 结构中进行传送的
-
缓冲区减到0之后, 发送方就不再发送数据, 转换为 “投石问路”, 每隔一段时间就发送窗口探测数据段,使接收端把窗⼝⼤⼩告诉发送端.
-
当缓冲区把数据消化掉之后, 就又进行传输
6) 拥塞控制 (安全机制)
这个机制是为了防止一开始不明确当前网络环境就发送大量的数据报, 会造成 “雪上加霜” 的情境.
由于滑动窗口机制的存在, 所以发送方无时不想使得窗口变得更大, 增加网络吞吐量.
因此, 产生了拥塞控制来控制每次数据传输的窗口大小. 同时引入慢启动机制进行控制
过程
- 引入"拥塞窗口" 概念
- 刚开始拥塞窗口只有1, 一次只能发送一个数据报
- 随着成功发送, 每次收到一个 ACK 确认报文, 都会使得拥塞窗口指数增加, 也就是说每次能够发送的数据条数指数增加
- 随着拥塞窗口的增大, 网络也会随之拥堵起来, 当拥塞窗口的大小达到慢启动的阈值后, 就变为线性增长
- 线性增长到一定大小, 会发生丢包现象, 此时就会重新进行规划拥塞窗口
- 旧版本 (慢启动): 拥塞窗口从0开始, 再次经历指数 - 线性 - 达到阈值停止的过程
- 新版本 (快恢复): 拥塞窗口从崩溃的1/2处重新进行线性增长, 然后重新达到阈值
7) 延迟应答 (效率机制)
为了提高 “一问一答” 式的效率, 衍生出 “多问一答”, 也即延迟应答机制
这种机制同样是通过确认序号进行操作.
- 在收到多次数据报之后
- 只返回最大序号的 ACK 确认应答报文
- 可以覆盖性应答之前小序号的数据报
8) 捎带应答 (效率机制)
这种机制体现在 “三次握手” 中最为明显, 服务器收到请求后, 既要发送 ACK 确认收到请求, 还要发送 SYN 确认连接报文, 这两个报文的发送时间又相差不了多久, 所以就发生了, SYN + ACK 报文一次同时发送两个报文的情况, 称为 “捎带应答”.
普遍的情况是发生在一问一答式中, 可以通过一次发送, 同时发送 ACK 与 应答报文. (也即尽可能将能够合并的报文合并发送)
但这并不是发送了两个数据报, 仍然只是一个, 只是将应答报文的 ACK 标志位置为1
为什么四次挥手不能合并为三次挥手?
因为第二次\第三次挥手发送的 FIN 与 ACK 报文之间的时间差距较大, 所以不能进行合并.
但是可能由于延时应答和捎带应答的存在, 可能会合并为三次挥手.
9) 面向字节流 – 粘包问题
由于 TCP 是面向字节流的传输协议, 并不是像 UDP 一样每次都发送一个完整的数据报, 所以 TCP 的数据接收方只会收到一大串的数据和他们的序号, 使得接收方不能知道每个数据的起始和结束位置, 所以产生了粘包问题.
粘包问题就像放在蒸笼上挨得近的一个一个馒头, 等到蒸好拿出来的时候, 馒头就会彼此粘连, 不分彼此.
解决粘包问题的关键 – 分隔出两个包的边界
- 约定一个正文内容中不会出现的符号作为数据之间的分隔符
- 在每个应用层数据报前加上一段空间用来表示这段数据报的长度
10) 异常情况
进程崩溃终止
这种情况, 会触发回收文件资源, 因为 TCP 连接的生命周期长于进程, 所以在进程崩溃后, TCP 仍然未断开连接, 仍然可以完成四次挥手的过程, 安全退出.
其中一方按照流程关机
关机前会由系统对于所有进程进行强行关闭, 所以此时也会进行四次挥手, 但是可能由于挥的不是特别快, 但就算再慢也能够发送一个 FIN 报文出去, 另一方会发送出 FIN 与 ACK报文, 但是在没有收到后续的 ACK 报文后, 就会触发超时重传, 但是重传之后还是没有回应, 那么另一方也会单方面释放资源.
其中一方突然断电
首先, 机器瞬间关机会导致来不及进行四次挥手, 就连发送一次 FIN 报文的时间都没有.
分为:
-
发送方断电
接收方本来就是在阻塞等待消息, 但是在一段时间后仍然接收不到消息, 就会触发==“心跳包”==机制进行询问, 看接收方是否还在.
-
接收方断电
-
发送方在发送几个数据报之后一直收不到 ACK 报文, 那就会触发==“超时重传”==机制,
-
但是仍然没有反应, 就会将报文中的 RST (重置) 标志位置为1, 将以往发送过的数据都清空, 希望能够得到发送方的 ACK 报文
-
如果此时仍然没有, 那就会单方面断开连接.
-
3. IP协议
3.1 格式
4位协议: 表明当前是IPv4还是Ipv6
4位首部长度: 单位是 “4字节”, 表明当前IP报文头部有多长, 4位能够表示的最大数字是15, 乘以4最大是60, 所以 IP 报头最大长度是60字节
8位服务类型: 只有4位有效, 而且这4位彼此之间冲突
就是说只能有一位为1:
- 最大吞吐量: 一次能够收发相对大量的数据
- 最小延时: 每次收发的延迟最低
- 最高可靠性: 没有 TCP 一样可靠, 但是仍有自己的可靠性机制
- 最小成本: 硬件设备的开销最低
16位总长度: IP数据报整体的长度
3位标志位: 只有两位有效, 其中一位是保留位
一位是表示当前的数据报有没有拆包;
一位是表示结束标记
13位片偏移: 描述当前报文分片在整体报文中的位置, 实际偏移的字节数是这个值*8得到的, 所以除了最后一个分片, 其他的分片长度都需要是8的整数倍
8位生存时间(Time To Live): 单位是 “次数”, 是指当前报文在整个网络路径中剩余能够转发的次数
8位协议: 表示传输层使用哪个协议
16位首部校验和: 表示 IP 报文首部的校验和
载荷部分的校验和由 UDP\TCP 自己处理
32位源\目的IP地址: 表示收\发地址, 是设备在网络中的唯一标识
3.2 IP 地址管理
IP 地址本质上是32位二进制数, 由于表示起来太长, 就使用了==“点分十进制”==进行表示: 192.168.1.1
- IP 地址是网络中设备的唯一标识符, 用来确定一台主机在网络中的位置
- 随着网络设备的增加, 32位的 IP 地址呈现不够用的姿态, 产生了 动态分配, NAT, IPv6 等手段
1) 网段划分
根据子网掩码, 全1,(即255)为网络号, 全0,(即0)为主机号
同一个网络号的网络称为同一个 “子网”
但是为每一个子网中的主机设置不同的网络号和主机号是一件繁琐的事情, 所以路由器中就有 “DHCP” 这种功能, 能够自动为每一个子网中的主机进行分配 IP 地址.
2) 解决 IP 地址不够用
-
动态分配
因为全世界的设备不是同时都是上网状态, 比如中国是白天的时候, 美国就是黑夜, 这时美国上网的设备会明显减少, 所以就将之前分配给美国的 IP 地址在他们上网设备大量减少的时候, 分配给中国的设备进行使用.
但是这种方法并不能从根本上解决, 而且带来的提升十分有限.
-
NAT
这是一种装在路由器上进行使用的软件, 能够对于同一子网中的主机进行 IP 地址映射, 这样就可以使得一个 IP 地址为大量的主机所使用.
过程:
NAT 机制, 是在路由器端使用两个IP地址进行的替换.
具体来说, 就是一个路由器维护着一个 LAN 口, 一个 WAN 口
- LAN 口是子网 IP
- WAN 口是公网 IP
在进行替换的时候, 是将 子网中的设备的IP, 即 LAN 口的地址, 替换为公网 IP, 也即 WAN 口地址
但是这种方法并不能从根本上解决, 而且带来的提升有限, 较 “动态分配” 提升较大
-
IPv6
这种方式从根本上解决了 IP 地址不够用的问题, 但是由于与 IPv4并不兼容, 所以想要使用 IPv6 上网, 需要更换全部的硬件, 所以目前并不是主流, 尽管在我国已经占了半壁江山, 但是流量并不是很大.
Ipv6使用了 32 位进行表示 IP 地址, 可以为地球上的每一粒沙子分配一个 IP 地址
3) 私有 IP 地址 & 公网 IP 地址
IP 地址在 NAT 机制的划分下, 分为 私网 与 公网.
私网:
- 10.*
- 172.16.* - 172.31.*
- 192.168.*
公网:
剩余的 IP 地址均为公网 IP
4) 特殊 IP
127.* : 环回IP, 测试使用的 IP
主机号全0: 表示==“这个网段”==
主机号全1: 表示==“广播地址”==, 也就是这个网络号中的所有设备都能够收到这个地址发来的消息
手机投屏就是使用了 “广播”
5) 网络选择
网络选择的过程就像地图软件不普及时的 问路, 是对于一个目的地的探索性询问, 一次询问可能问不出来终点位置, 但是每一次的询问都使自己更加接近终点
-
牵扯出了 MAC 地址, 这个地址描述的是数据报在每个结点之间走到的地址, 是一个==“区间地址”==
不同于 IP地址, IP 地址描述的是==“起点-终点地址”==
-
每个路由器对于自己周围的网络环境有一个基本的认识, 维护一张表, 就像一个区域的居民对于本区域的地标性建筑有一定的认识, 使得外地人来问路可以给出基于当前地点的模糊性方向传输层网络协议