UDP 编程
# UDP 编程
和 TCP 编程相比,UDP 编程就简单得多,因为 UDP 没有创建连接,数据包也是一次收发一个,所以没有流的概念。
在 Java 中使用 UDP 编程,仍然需要使用 Socket,因为应用程序在使用 UDP 时必须指定网络接口(IP)和端口号。注意:UDP 端口和 TCP 端口虽然都使用 0~65535,但他们是两套独立的端口,即一个应用程序用 TCP 占用了端口 1234,不影响另一个应用程序用 UDP 占用端口 1234。
# 服务器端
在服务器端,使用 UDP 也需要监听指定的端口。Java 提供了 DatagramSocket 来实现这个功能,代码如下:
package chapter20;
import java.net.DatagramPacket;
import java.net.DatagramSocket;
import java.nio.charset.StandardCharsets;
public class UDPDemo1Server {
public static void main(String[] args) throws Exception {
DatagramSocket ds = new DatagramSocket(7777); // 监听指定端口
System.out.println("server is running....");
while (true){
// 数据缓冲区
byte[] buffer = new byte[1024];
DatagramPacket packet = new DatagramPacket(buffer, buffer.length);
//收取一个UDP数据包。收取到的数据存储在buffer中,由packet.getOffset(), packet.getLength()指定起始位置和长度
ds.receive(packet);
// 将其按UTF-8编码转换为String
String s = new String(packet.getData(), packet.getOffset(), packet.getLength(), StandardCharsets.UTF_8);
System.out.println("Received data \" " + s + " \" from client");
// 发送数据给客户端
byte[] data = "ACK".getBytes(StandardCharsets.UTF_8);
packet.setData(data);
ds.send(packet);
}
}
}
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
我们解读下这段代码。服务器端首先使用如下语句在指定的端口监听 UDP 数据包:
DatagramSocket ds = new DatagramSocket(7777);
如果没有其他应用程序占据这个端口,那么监听成功,我们就使用一个无限循环来处理收到的 UDP 数据包:
while (true){
...
}
2
3
要接收一个 UDP 数据包,需要准备一个 byte[] 缓冲区,并通过 DatagramPacket 实现接收:
byte[] buffer = new byte[1024];
DatagramPacket packet = new DatagramPacket(buffer, buffer.length);
ds.receive(packet);
2
3
假设我们收取到的是一个 String,那么,通过 DatagramPacket 返回的 packet.getOffset() 和 packet.getLength() 确定数据在缓冲区的起止位置:
String s = new String(packet.getData(), packet.getOffset(), packet.getLength(), StandardCharsets.UTF_8);
当服务器收到一个 DatagramPacket 后,通常必须立刻回复一个或多个 UDP 包,因为客户端地址在 DatagramPacket 中,每次收到的 DatagramPacket 可能是不同的客户端,如果不回复,客户端就收不到任何 UDP 包。
发送 UDP 包也是通过 DatagramPacket 实现的,发送代码非常简单:
byte[] data = ...
packet.setData(data);
ds.send(packet);
2
3
# 客户端
和服务器端相比,客户端使用 UDP 时,只需要直接向服务器端发送 UDP 包,然后接收返回的 UDP 包:
package chapter20;
import java.net.DatagramPacket;
import java.net.DatagramSocket;
import java.net.InetAddress;
public class UDPDemo2Client {
public static void main(String[] args) throws Exception{
DatagramSocket ds = new DatagramSocket();
ds.setSoTimeout(1000);
ds.connect(InetAddress.getByName("localhost"), 7777); // 连接指定服务器和端口
// 发送数据给服务器
byte[] data = "Hello".getBytes();
DatagramPacket packet = new DatagramPacket(data, data.length);
ds.send(packet);
System.out.println("Send data \"Hello\" from server.");
// 接受服务器返回的数据
byte[] buffer = new byte[1024];
packet = new DatagramPacket(buffer, buffer.length);
ds.receive(packet);
String resp = new String(packet.getData(), packet.getOffset(), packet.getLength());
System.out.println("Received data \" " + resp + "\" from server.");
ds.disconnect();
}
}
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
客户端打开一个 DatagramSocket 使用以下代码:
DatagramSocket ds = new DatagramSocket();
ds.setSoTimeout(1000);
ds.connect(InetAddress.getByName("localhost"), 7777);
2
3
客户端创建 DatagramSocket 实例时并不需要指定端口,而是由操作系统自动指定一个当前未使用的端口。紧接着,调用 setSoTimeout(1000) 设定超时 1 秒,意思是后续接收 UDP 包时,等待时间最多不会超过 1 秒,否则在没有收到 UDP 包时,客户端会无限等待下去。这一点和服务器端不一样,服务器端可以无限等待,因为它本来就被设计成长时间运行。
注意到客户端的 DatagramSocket 还调用了一个 connect() 方法“连接”到指定的服务器端。不是说 UDP 是无连接的协议吗?为啥这里需要 connect()?
这个 connect() 方法不是真连接,它是为了在客户端的 DatagramSocket 实例中保存服务器端的 IP 和端口号,确保这个 DatagramSocket 实例只能往指定的地址和端口发送 UDP 包,不能往其他地址和端口发送。这么做不是 UDP 的限制,而是 Java 内置了安全检查。
如果客户端希望向两个不同的服务器发送 UDP 包,那么它必须创建两个 DatagramSocket 实例。
后续的收发数据和服务器端是一致的。通常来说,客户端必须先发 UDP 包,因为客户端不发 UDP 包,服务器端就根本不知道客户端的地址和端口号。
如果客户端认为通信结束,就可以调用 disconnect() 断开连接:
ds.disconnect();
注意到 disconnect() 也不是真正地断开连接,它只是清除了客户端 DatagramSocket 实例记录的远程服务器地址和端口号,这样,DatagramSocket 实例就可以连接另一个服务器端。
# 测试
先运行 UDPDemo1Server,然后运行 UDPDemo2Client。
UDPDemo2Client 执行的结果:
Send data "Hello" from server.
Received data " ACK" from server.
2
UDPDemo1Server 运行的结果:
server is running....
Received data " Hello " from client
2
# 小结
使用 UDP 协议通信时,服务器和客户端双方无需建立连接:
- 服务器端用
DatagramSocket(port)监听端口; - 客户端使用
DatagramSocket.connect()指定远程地址和端口; - 双方通过
receive()和send()读写数据; DatagramSocket没有 IO 流接口,数据被直接写入byte[]缓冲区。