文章目录
UDP是什么?
它是一种用户数据报协议,又称用户数据报文协议,是一个简单的面向数据报的传输层协议,正式规范为RFC 768;UDP 为应用程序提供了一种无需建立连接就可以发送封装的 IP 数据报的方法
UDP核心API
- DatagramSocket:用于接收与发送UDP信息的类(发送/接收UDP包),不同于TCP, UDP没有合并到Socket api中, 该类即是服务器也是客户端.
- DatagramPacket:即UDP报文封装类,用于处理报文, 将byte数组、目标地址、目标端口等数据包装成报文或将报文解为byte数组,它是UDP的发送实体和接收实体。
至于目标地址、目标端口的具体含义取决于该数据报文是被发送还是被接收。若是要发送的数据报文,则表示接收端的ip和端口;若是接收到的数据报文,则表示发送端的ip和端口。
DatagramSocket构造方法介绍
- DatagramSocket():创建简单实例,不指定端口与IP,让系统自动分配
- DatagramSocket(int port):创建监听固定端口的实例
- DatagramSocket(int port,InetAddress localAddr):创建固定端口指定IP的实例
DatagramSocket常用方法
- receive(DatagramPacket d):接收一个数据报文
- send(DatagramPacket d):发送一个数据报文
- setSoTimeout(int timeout):设置超时时间,单位毫秒
- close():关闭、释放资源
DatagramPacket的构造方法
- DatagramPacket(byte buf[], int length, InetAddress address, int port):通常用于发送数据报文时使用,前2个参数指定buf的使用区间,后面2个参数指定目标机器的地址与端口.
- DatagramPacket(byte[]buf,int offset, int length,InetAddress address,int port):通常用于发送数据报文时使用,前3个参数指定buf的使用区间,后面2个参数指定目标机器的地址与端口.
- DatagramPacket(byte[buf,int length,SocketAddress address]):通常用于发送数据报文时使用,前2个参数指定buf的使用区间,SocketAddress 相当于InetAddress+Port.
- DatagramPacket(byte buf[], int length) :通常用于接收数据报文时使用,参数用于指定buf和buf的长度.
DatagramPacket的常用方法
- setData(byte[]buf,int offset,int length):设置指定长度的数据
- setData(byte[]buf):设置buf的完整数据
- setLength(int length):设置buf数据的长度
- getData():获取报文中的数据
- getOffset():获取报文中数据buf的偏移量
- getLength():获取报文中数据buf的长度
- setAddress(InetAddress iaddr):设置目标地址(发送端用于设置数据报的接收端地址)
- setPort(int port):设置目标端口(发送端用于设置数据报的接收端端口)
- getAddress():获取目标地址(发送端地址)
- getPort():获取目标端口(发送端端口)
- setSocketAddress(SocketAddress adress):设置目标地址+端口
- getSocketAddress():获取目标地址+端口
通常get方法用于获取该数据报的发送者信息,而set方法用于设置数据报的接收端信息,当然如果你使用多参数的构造方法的话,可以减少set方法的使用。
单播、广播、多播
- 单播:就是点对点发送信息,不被其他点所感知。
- 广播:给所有端发送信息,会受到路由器的隔离限制,仅仅在同一局域网内有效。
- 多播:也称为组播,是向一组点发送信息。
案例介绍
该案例分为UDP提供者(即被搜索的设备,例如局域网中的智能设备)和UDP搜索者(例如局域网中的手机)。
需要完成的功能是通过手机查找局域网中智能设备的唯一标识,假设唯一标识是一串UUID值,查找后打印出来。
UDP提供者需要实现的功能是循环的监听特定端口,然后解析收到的数据,判断该数据是否符合预定的格式,从中获取到发送者的响应端口,并将唯一标识UUID值响应给UDP的搜索者。
UDP搜索者需要实现的功能就是监听特定端口和发送局域网广播,发送广播的时候将监听端口设置在数据中,因此需要先开启监听完成后,才能发送广播,一旦收到响应数据,就可以解析设备信息了。
下面开始编码,首先来一个MessageCreator类,用于封装和解析端口以及设备唯一标识
/**
* 消息创建者
*/
public class MessageCreator {
private static final String SN_HEADER = "Receive Port I'm UDPProvider (SN):"; //UDPProvider 回复的sn
private static final String PORT_HEADER = "I'm UDPSearcher,Please send to (Port):"; //UDPSearcher的响应端口
public static String buildWithPort(int port) {
return PORT_HEADER + port;
}
public static int parsePort(String data){
if (data.startsWith(PORT_HEADER)) {
return Integer.parseInt(data.substring(PORT_HEADER.length()));
}
return -1;
}
public static String buildWithSn(String sn) {
return SN_HEADER + sn;
}
public static String parseSn(String data){
if (data.startsWith(SN_HEADER)) {
return data.substring(SN_HEADER.length());
}
return null;
}
}
然后是UDPProvider类
import java.io.IOException;
import java.net.DatagramPacket;
import java.net.DatagramSocket;
import java.util.UUID;
/**
* UDP 提供者,用于提供服务
*/
public class UDPProvider {
public static void main(String[] args) throws IOException {
//生成一份唯一标识
String sn = UUID.randomUUID().toString();
Provider provider = new Provider(sn);
provider.start();
//读取任意字符退出
System.in.read();
provider.exit();
}
private static class Provider extends Thread {
private final String sn;
private boolean done = false;
private DatagramSocket ds = null;
public Provider(String sn) {
super();
this.sn = sn;
}
@Override
public void run() {
super.run();
System.out.println("UDPProvider Started");
try {
// 作为接收者,指定一个端口用于数据接收
ds = new DatagramSocket(20000);
while (!done) {
//构建接收实体
final byte[] buf = new byte[512];
DatagramPacket receivePack = new DatagramPacket(buf, buf.length);
//开始接收
ds.receive(receivePack);
//打印接收到的信息与发送者的信息
String ip = receivePack.getAddress().getHostAddress();//发送者的ip
int port = receivePack.getPort();//发送者的端口
int dataLen = receivePack.getLength();//数据包长度
String data = new String(receivePack.getData(), 0, dataLen);//数据内容
//打印发送者ip/端口/数据
System.out.println("UDPProvider receive from ip:" + ip + "\tport:" + port + "\tdata:" + data);
//解析响应端口号
int responsePort = MessageCreator.parsePort(data);
if (responsePort != -1) {
//构建一份回送数据将设备的sn返回给搜索者
String responseData = MessageCreator.buildWithSn(sn);
byte[] responseDataBytes = responseData.getBytes();
//直接根据发送者构建一份回送信息
DatagramPacket responsePacket = new DatagramPacket(responseDataBytes,
responseDataBytes.length,
receivePack.getAddress(),//发送者的ip拿来指定为接收端ip
responsePort); //回送到指定端口
ds.send(responsePacket);
}
}
} catch (Exception ignored) {
} finally {
close();
}
//完成
System.out.println("UDPProvider Finished");
}
private void close() {
if (null != ds) {
ds.close();
ds = null;
}
}
/**
* 结束
*/
void exit() {
done = true;
close();
}
}
}
最后是UDPSearcher类
import java.io.IOException;
import java.net.*;
import java.util.ArrayList;
import java.util.List;
import java.util.concurrent.CountDownLatch;
/**
* UDP 搜索者
*/
public class UDPSearcher {
//监听回送端口号
private static final int LISTENER_PORT = 30000;
public static void main(String[] args) throws IOException, InterruptedException {
System.out.println("UDPSearcher Started");
//开启监听30000端口
Listener listener = listen();
//发送广播
sendBroadcast();
//读取任意信息结束
System.in.read();
List<Device> devices = listener.getDevicesAndClose();
for (Device device : devices) {
System.out.println("Device:" + device.toString());
}
System.out.println("UDPSearcher Finished");
}
/**
* 监听UDP提供者的回应
*/
private static Listener listen() throws InterruptedException {
System.out.println("UDPSearcher start listen");
CountDownLatch countDownLatch = new CountDownLatch(1);
Listener listener = new Listener(LISTENER_PORT, countDownLatch);
listener.start();
countDownLatch.await(); //等待监听启动完成,这里是阻塞的
return listener;//启动完成就返回监听
}
/**
* 发送广播开始搜索UDP提供者
*
* @throws IOException
*/
private static void sendBroadcast() throws IOException {
System.out.println("UDPSearcher sendBroadcast Started");
// 作为搜索方,无需指定端口,让系统自动分配
DatagramSocket ds = new DatagramSocket();
//发送一份请求数据,暴露监听端口
String requestData = MessageCreator.buildWithPort(LISTENER_PORT);
byte[] requestDataBytes = requestData.getBytes();
//创建一个DatagramPacket
DatagramPacket requestPacket = new DatagramPacket(requestDataBytes,
requestDataBytes.length);
//指定接收方的ip地址,由于用的是本机接收,所以可以用localhost
//requestPacket.setAddress(InetAddress.getLocalHost());
//或者指定接收端具体ip也可以,这里用的是受限的广播地址
//requestPacket.setAddress(InetAddress.getByName("255.255.255.255"));
//给特定网段广播地址发送
requestPacket.setAddress(InetAddress.getByName("192.168.1.255"));
//指定接收方的端口号
requestPacket.setPort(20000);
//开始发送广播
ds.send(requestPacket);
ds.close();
//发送广播完成
System.out.println("UDPSearcher sendBroadcast Finished");
}
/**
* 目标设备(UDP提供者)
*/
private static class Device {
final int port;
final String ip;
final String sn;
public Device(int port, String ip, String sn) {
this.port = port;
this.ip = ip;
this.sn = sn;
}
@Override
public String toString() {
return "Device{" +
"port=" + port +
", ip='" + ip + '\'' +
", sn='" + sn + '\'' +
'}';
}
}
//监听线程
private static class Listener extends Thread {
private final int listenPort;
private final CountDownLatch countDownLatch;
private final List<Device> devices = new ArrayList<>();
private boolean done = false;
private DatagramSocket ds = null;
public Listener(int listenPort, CountDownLatch countDownLatch) {
super();
this.listenPort = listenPort;
this.countDownLatch = countDownLatch;
}
@Override
public void run() {
super.run();
//通知已启动
countDownLatch.countDown();
try {
//监听回送端口
ds = new DatagramSocket(listenPort);
while (!done) {
//构建接收实体
final byte[] buf = new byte[512];
DatagramPacket receivePack = new DatagramPacket(buf, buf.length);
//开始接收
ds.receive(receivePack);
//打印接收到的信息与发送者的信息
String ip = receivePack.getAddress().getHostAddress();//发送者的ip
int port = receivePack.getPort();//发送者的端口
int dataLen = receivePack.getLength();//数据包长度
String data = new String(receivePack.getData(), 0, dataLen);//数据内容
System.out.println("UDPSearcher receive from ip:" + ip + "\tport:" + port + "\tdata:" + data);
//解析sn并添加设备
String sn = MessageCreator.parseSn(data);
if (null != sn) {
Device device = new Device(port, ip, sn);
devices.add(device);
}
}
} catch (Exception e) {
} finally {
close();
}
System.out.println("UDPSearcher listener finished");
}
private void close() {
if (null != ds) {
ds.close();
ds = null;
}
}
List<Device> getDevicesAndClose() {
done = true;
close();
return devices;
}
}
}
测试
首先开启UDPProvider,用于监听广播并将设备信息发送给搜索者
然后在开启UDPSearcher,用于发送广播并监听自己的特定端口来获取设备信息
可以看到,同时搜索到了2台设备。