前言
在前两节中我们介绍了UDP数据报套接字编程,但是并没有对UDP进行详细介绍,本节中我们将会详细介绍传输层中的UDP协议。
一、什么是UDP?
UDP工作在传输层,用于程序之间传输数据的。数据一般包含:文件类型,视频类型,jpg图片等。
1.1 基本概念:
UDP 的全称: 用户数据报协议(UDP,User Datagram Protocol),UDP 为应用程序提供了一种无需建立连接就可以发送封装的 IP 数据包的方法。如果程序员选择的是 UD, 而不是 TCP 的话,那么该应用程序相当于就是和 IP 直接打交道的。
从应用程序传递过来的数据,会附加上多路复用/多路分解的源和目的端口号字段,以及其他字段,然后将形成的报文传递给下一层(网络层),网络层将传输层报文段封装到 IP 数据报中,然后尽力而为的交付给目标主机。最关键的一点就是,使用 UDP 协议在将数据报传递给目标主机时,发送方和接收方的传输层实体间是没有握手的。正因为如此,UDP 被称为是无连接的协议。
1.2 UDP的特点
UDP传输的过程类似于寄信。
无连接
知道对端的IP和端口号就直接进行传输,不需要建立连接。
不可靠
没有任何安全机制,发送端发送数据报以后,如果因为网络故障该段无法发到对方,UDP协议层也不会给应用层返回任何错误信息;
面向数据报
应用层交给UDP多长的报文,UDP原样发送,既不会拆分,也不会合并。比如:用UDP传输100个字节的数据:
如果发送端一次发送100个字节,那么接收端也必须一次接收100个字节;而不能循环接收10次, 每次接收10个字节。
缓冲区
UDP只有接收缓冲区,没有发送缓冲区:
- UDP没有真正意义上的 发送缓冲区。发送的数据会直接交给内核,由内核将数据传给网络层协议进行后续的传输动作;
- UDP具有接收缓冲区,但是这个接收缓冲区不能保证收到的UDP报的顺序和发送UDP报的顺序一致;如果缓冲区满了,再到达的UDP数据就会被丢弃;
大小受限
UDP协议首部中有一个16位的最大长度。也就是说一个UDP能传输的数据最大长度是64K(包含UDP首部)
二、UDP 报文结构
如下图所示: UDP 的报文结构,每个 UDP 报文分为 UDP 报头和 UDP 数据区两部分。报头由 4 个 16 位长(2 字节)字段组成,分别说明该报文的源端口、目的端口、报文长度和校验值。
16位源端口号(Source Port) :
这个字段占据 UDP 报文头的前 16 位,通常包含发送数据报的应用程序所使用的 UDP 端口。接收端的应用程序利用这个字段的值作为发送响应的目的地址。这个字段是可选项,有时不会设置源端口号。没有源端口号就默认为 0 ,通常用于不需要返回消息的通信中。
16位目标端口号(Destination Port)
表示接收端端口,字段长为 16 位。
长度
该字段占据 16 位,表示 UDP 数据报长度,包含 UDP 报文头和 UDP 数据长度。因为 UDP 报文头长度是 8 个字节,所以这个值最小为 8,最大长度为 65535 字节。
16位校验和(Checksum)
UDP 使用校验和来保证数据安全性,UDP 的校验和也提供了差错检测功能,差错检测用于校验报文段从源到目标主机的过程中,数据的完整性是否发生了改变。常见的检验方法:循环冗余检测(CRC)。
- 为什么 UDP 会提供差错检测的功能?
这其实是一种 端到端 的设计原则,这个原则说的是要让传输中各种错误发生的概率降低到一个可以接受的水平。 - 如何基于校验和来完成数据校验呢?
- 发送方,把要发送的数据整理好(称为 data1),通过一定的算法,计算出校验和 checksum1
- 发送方把 data1 和 checksum1 一起通过网络发送出去.
- 接收方收到数据, 收到的数据称为 data2(数据可能和 data1 就不一样了),收到数据cHecksum1
- 接收方再根据 data2 重新计算校验和 (按照相同的算法),得到 checksum2
- 对比 checksum1 和 checksum2 是否相同,如果不同, 则认为 data2 和 data1 一定不相同.如果checksum1 和 checksum2 相同,则认为 data1 和 data2 大概率是相同的(理论上存在不同的可能性,概率比较低,工程上忽略不计)
三、通过代码理解UDP的特点
3.1 无连接
UDP协议本身不会储存对端的信息,要在发送数据的时候,才会显示指定要传输给谁?
DatagramPacket responsePacket = new DatagramPacket(response.getBytes(),response.getBytes().length,
requestPacket.getSocketAddress());
// 构造这个数据报,需要指定数据内容,也指定下一个数据报要发给谁
socket.send(responsePacket);
3.2 不可靠
3.3 面向数据报
DatagramPacket responsePacket = new DatagramPacket(response.getBytes(),response.getBytes().length,
requestPacket.getSocketAddress());
// 构造这个数据报,需要指定数据内容,也指定下一个数据报要发给谁
socket.send(responsePacket);
3.4 全双工
通过一个socket,既可以send,也可以receive.
socket.send(requestPacket);
// 3.尝试服务器返回的响应了
DatagramPacket responsePacket = new DatagramPacket(new byte[4096],4096);
socket.receive(responsePacket);
上一节中的参考代码(https://blog.csdn.net/2301_80653026/article/details/139443856):
UDP客户端
import java.io.IOException;
import java.net.DatagramPacket;
import java.net.DatagramSocket;
import java.net.SocketException;
import java.nio.channels.DatagramChannel;
/**
* @author Zhang
* @date 2024/5/2015:38
* @Description:
*/
public class UdpEchoServer {
//创建一个datagramSocket对象,这是后续操作网卡的基础
private DatagramSocket socket = null;
public UdpEchoServer(int port) throws SocketException {
//此写法是手动指定端口
socket = new DatagramSocket(port);
// 此写法是让服务器自动分配端口
// socket = new DatagramSocket();
}
//通过这个方法来启动服务器
public void start() throws IOException {
System.out.println("服务器启动");
while(true){
// 1. 读取请求并解析
/**
* DatagramPacket这个队形来承载从网卡这边读取的数据,
* 收到的数据需要一个内存空间来保存这个数据
* DatagramPacket 内部不能自动分配空间,需要程序员手动创建,交给DatagramPacket进行处理
*/
DatagramPacket requestPacket = new DatagramPacket(new byte[4096],4096);
socket.receive(requestPacket); //当服务器一旦启动,就会立即执行receive方法,入伏哦客户端请求还没到,receive就会阻塞到客户端请求到达
// 当完成receive后,数据以二进制的形式存储到DatagramPacket
// 要想把这里的数据显示出来,好需要把这个二进制转变成字符串
String request = new String(requestPacket.getData(),0,requestPacket.getLength());
// 2.根据请求计算响应(一般服务器都会经理的过程)
// 由于此处是回显服务器,请求是啥样,响应就是啥样
String response = process(request);
// 3. 把响应写会到客户端
// 写一个响应对象,DatagramPacket
// 往DatagramPacket 构造刚才数据,并通过 sent 返回
DatagramPacket responsePacket = new DatagramPacket(response.getBytes(),response.getBytes().length,requestPacket.getSocketAddress());
// 构造这个数据报,需要指定数据内容,也指定下一个数据报要发给谁
socket.send(responsePacket);
// 4.打印一个日志,把这次数据交互的详情打印出来
System.out.printf("[%s:%d] req = %s, resp = %s\n",requestPacket.getAddress().toString(),
requestPacket.getPort(),request,response);
}
}
public String process(String request){
return request;
}
public static void main(String[] args) throws IOException {
UdpEchoServer server = new UdpEchoServer(9090);
server.start();
}
}
服务端
import java.io.IOException;
import java.net.*;
import java.util.Scanner;
/**
* @author Zhang
* @date 2024/5/2015:38
* @Description:
*/
public class UdpEchoClient {
private DatagramSocket socket = null;
private String serverIP = "";
private int serverPort = 0;
public UdpEchoClient(String ip,int port) throws SocketException {
// 创建这个对象,不能手动指定端口
socket = new DatagramSocket();
//由于UDP自身不会持有对端的信息,就需要在应用程序里,吧对端的情况给记录下来
serverIP = ip;
serverPort = port;
}
public void start() throws IOException {
System.out.println("客户端启动!");
Scanner scanner = new Scanner(System.in);
while (true){
// 1.从控制台读取数据,作为请求
System.out.print("->");
String request = scanner.next();
// 2. 把请求内容构造成 DatagramPacket 对象,发送给服务器
DatagramPacket requestPacket = new DatagramPacket(request.getBytes(), request.getBytes().length,
InetAddress.getByName(serverIP), serverPort);
socket.send(requestPacket);
// 3.尝试服务器返回的响应了
DatagramPacket responsePacket = new DatagramPacket(new byte[4096],4096);
socket.receive(responsePacket);
// 4.把响应转换成字符串,并先显示出来
String response = new String(responsePacket.getData(),0,responsePacket.getLength());
System.out.println(response);
}
}
public static void main(String[] args) throws IOException {
UdpEchoClient client = new UdpEchoClient("127.0.0.1", 9090);
client.start();
}
}
总结
UDP特点:无连接、不可靠、面向数据报、全双工。