1 简述
用户数据报协议(User Datagram Protocol) 简称UDP协议,相对于TCP协议,它的协议简单很多,因为它即不需要建立连接,也不需保证可靠性,和TCP协议的连接性、可靠性,形成鲜明对比,也正因如此,反而突显了它的独特性,在传输层跟TCP并驾齐驱的地位。
HTTP/3 在2022-06-06正式发布,它的数据传输基于UDP协议,可以看出该协议旺盛的生命力。
本文对UDP协议关键概念进行整理,分析它如何进行网络间数据传递。
2 特点
2.1 简单性
个人理解,简单是它最主要的特点, 它没有所谓的握手、断开、重置等逻辑,也不提供流量控制,当然更没有数据重传机制,一会儿看了它的"协议格式",就知道它是如何精炼。
2.2 不可靠
数据报文在传输过程中,如果有分段丢失, 不能重构完整报文,则直接丢弃该报文,也不能保证报文间的时序性。
2.3 速度快
因为它的数据传输方式非连接、不可靠,也不控制流量,因此传输速度快,这是其主要优点,适用于需要大量、及时但对数据完整性要求不是很高的业务场景。
3 协议格式
通过Wireshark工具,对网络数据抓包,会发现它的包(frame)数据被一层一层包裹,而我们(进程)真实传递的"数据"被包裹在最里面的那一层。最常见的数据包是3层: 最外面是Ethernet(以太帧)的协议头,中间是IP协议头, 最里面是UDP协议头和包裹的真实数据,其中Ethernet帧、IP协议在上一篇"TCP协议"部分已叙述,下面只描述UDP协议格式。
3.1 UDP
UDP协议位于IP协议上层,被IP协议包裹,也就是IP分组数据区内容,下面是它的格式:
来源端口、目的端口:它和IP地址组合后,构成了完整的UDP发送、接收地址。
消息长度: 占2个字节,值为UDP分组的总长度(包含报头及数据区)。
该协议是不是很精简!
3.2 完整包
在网络中自始至终传递的是IP分组,在传递过程中,经过不同的网络,根据物理层真实的链路层协议,对IP分组进行打包传递,以下是局域网以太帧的完整格式:
4. 数据传输
从上图可以看出,客户端、服务端不需要建立、断开连接处理,它的数据传递,除receive数据,需要等待阻塞外,基本上没有阻塞环节,这也是UDP数据传递快的原因。
它以报文为单位进行数据传递,在报文超过MSS字节限制后,IP协议会对其进行分段,到达目的地后会重新构建报文,这个过程,在应用层无感知,接收端可能会丢失报文,但收到的报文,可以保证它的完整性、正确性。(前提:确保双方报文缓存大小一致)
再就是,以太网每帧数据传递有最大字节限制,约1.4k字节,有些物理层可能会更少,需视情况而定,IP协议标准规定一般不会低于576字节,如果网络较差, 建议报文尽量别太大,否则超过限值,IP协议就会进行分段处理, 只要其中一个IP分段没有收到,就会丢掉整个报文,影响数据包送达率。
5. 实践-java
下面提供阻塞方式处理例子,通过抓包可以检测,UDP的数据传输,不再需要握手建立连接,也不关心接受端是否能正确收到报文,除了网络原因导致的丢包外,如果接受端不能及时读取报文,就会出现"接受缓存区"溢出,最后导致后续的报文,被抛弃处理。
客户端:
package mtr.demo.tcp.blog;
import java.io.IOException;
import java.net.DatagramPacket;
import java.net.DatagramSocket;
import java.net.InetAddress;
public class UdpClient {
private static final int RECEIVE_BUFFER_SIZE = 10 * 1024 * 1024;
private static final int SEND_BUFFER_SIZE = 10 * 1024 * 1024;
private static final int READ_TIMEOUT_MILLISECONDS = 30000;
public static void main(String[] args) throws Exception {
// 创建socket
DatagramSocket socket = createDatagramSocket();
try {
// 发送报文
for(int i = 0; i < 3; i++) {
String msg = buildData(i * 10, 10);
DatagramPacket packet = new DatagramPacket(msg.getBytes(), msg.getBytes().length, InetAddress.getByName("192.168.0.106"), 8888);
socket.send(packet);
System.out.println(String.format("序列%d, 已发送:%s", i, msg));
}
// 接收报文
do {
DatagramPacket packet = new DatagramPacket(new byte[1024], 1024);
socket.receive(packet);
String msg = new String(packet.getData(), 0, packet.getLength(), "utf-8");
System.out.println("收到报文:" + msg);
} while(true);
} finally {
socket.close();
System.out.println("close udp socket");
}
}
/*
* 创建DatagramSocket
*/
static DatagramSocket createDatagramSocket() throws IOException {
// 创建DatagramSocket, 分配临时接口
DatagramSocket socket = new DatagramSocket();
socket.setReceiveBufferSize(RECEIVE_BUFFER_SIZE);
socket.setSendBufferSize(SEND_BUFFER_SIZE);
socket.setSoTimeout(READ_TIMEOUT_MILLISECONDS);
System.out.println("creata udp socket, local-port: " + socket.getLocalPort());
return socket;
}
static String buildData(int start, int len) {
StringBuilder builder = new StringBuilder();
for(int i = 0; i < len; i++ ) {
builder.append(String.format("%09d,", start + i));
}
return builder.toString();
}
}
服务端:
package mtr.demo.tcp.blog;
import java.net.DatagramPacket;
import java.net.DatagramSocket;
public class UdpServer {
private static final int RECEIVE_BUFFER_SIZE = 10 *1024 * 1024;
public static void main(String[] args) throws Exception {
DatagramSocket socket = new DatagramSocket(8888);
socket.setReceiveBufferSize(RECEIVE_BUFFER_SIZE);
try {
System.out.println("开始接受报文...");
DatagramPacket packet = new DatagramPacket(new byte[1024], 1024);
int count = 0;
while(true) {
count++;
// 接受
socket.receive(packet);
String msg = new String(packet.getData(), 0, packet.getLength());
System.out.println(String.format("收到报文, order:%d, size:%d, data:%s", count, packet.getLength(), msg));
// 回复
byte[] reply = String.format("receive data order:%d, size: %d", count, packet.getLength()).getBytes();
socket.send(new DatagramPacket(reply, reply.length, packet.getSocketAddress()));
}
} finally {
socket.close();
}
}
}