avatar

Socket编程-UDP

Socket 与TCP、UDP


Socket就是IP地址与端口的结合协议(RFC 793),一种地址与端口的结合描述协议。

Socket的作用与组成

  • 在网络传输中用于唯一标识两个端点之间的链接
  • 端点: 包括IP和Port

UDP

UDP最大长度:

UDP Header2Bytes来存储长度信息,所以UDP的长度应该是

$$ 2^{16}-1 = 65535 Bytes $$

自身协议(头部信息)占用 8 Bytes (2Bytes × 4种信息 )

$$ 65535-8 = 65507 Bytes $$

也就是当信息长度大于65507 Bytes时,用UDP传输会出现问题,需要做一次分包传输


UDP核心API

  1. API-DatagramSocket
  • 用于接收与发送UDP的类
  • 负责发送某一个UDP包,或者接收UDP包
  • 不同于TCP,UDP并没有合并到Socket API中
1
2
3
4
5
6
7
DatagramSocket()// 创建简单实例,不指定端口和IP
DatagramSocket(int port)// 创建监听固定端口的实例
DatagramSocket(int port ,InetAddress localAddr) // 创建固定端口指定IP的实例
receive(DatagramPacket d) // 接收
send(DatagramPacket d) // 发送
setSoTimeout(int timeout) // 设置超时,毫秒
close() // 关闭资源
  1. API-DatagramPacket
  • 用于处理报文
  • 将byte数组、目标地址、目标端口等数据包装成报文或者将报文拆卸成byte数组
  • 是UDP发送实体,也是接收实体
1
2
3
4
5
6
DatagramPacket(byte[] buf ,int offset ,int length ,InetAddress address ,int port)// 前三个参数指定buf的使用区间,后面是目标机器地址与端口
DatagramPacket(byte[] buf ,int length ,SocketAddress address )// 创建监听固定端口的实例,SocketAddress相当于InetAddress+Port
setData(byte[] buf ,int offset ,int length) // 指定buf中哪一部分信息是有效的
setData(byte[] buf) // 整个buf均被指定
setLength(int length) // 用于单独设置buf有效区间的长度
setAddress(InetAddress iaddr),setPort(int iport),setSocketAddress(SocketAddress address) // 设定IP地址和端口

UDP单播、广播、多播

  1. 单播:点对点
  2. 多播:点对多个点(局域网内部分点)
  3. 广播:点对所有设备

多播是在广播之后出现的,广播会导致大量的带宽被浪费。

广播地址运算:

  • IP地址 : 192.168.124.7
  • 子网掩码 : 255.255.255.192 -> 11111111.11111111.11111111.11000000
  • 可划分网段: 2^2 = 4个
  • 网段:0-63、64-127、128-191、192-255
  • 广播地址: 192.168.124.63(所在网段的最高位)
  • 两台主机广播地址不同,是不能进行广播通信的

UDP局域网搜索

假设一个场景,手机(Searcher)与其他互联设备(Provider)同处局域网内,Provider与Searcher共用一套交互协议(同时监听某一端口,遵循相同的传输协议等),Searcher希望通过广播找到某一特定的Provider,整套流程就可以基于UDP局域网搜索实现。

Socket_UDP_01.png

需要注意的点:

  • Searcher要先开启Port:30000监听。因为启动Searcher后,再开启端口的话,是有可能错过信息的,Searcher和Provider会立刻广播、立刻返回SN
  • Provider和Searcher在构建类后,必须要持续监听,同时可以随时停止,所以最好用线程实现。
  • 要先开启Provider,再启动Searcher。

1. MessageCreator实现

MessageCreator用来封装消息、解析消息。

1
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
32
33
public class MessageCreator {

private static final String SN_HEADER = "收到暗号,我是(SN):";
private static final String PORT_HEADER = "这是暗号,请回电端口(Port):";

// 创建信息
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;
}

// SN 创建信息方法
public static String buildWithSn(String sn) {
return SN_HEADER + sn;
}

// SN 解析信息
public static String parseSn(String data) {
if (data.startsWith(SN_HEADER)) {
return data.substring(SN_HEADER.length());
}

return null;
}
}

2. UDPProvider实现

UDPProvider类启动

1
2
3
4
5
6
7
8
9
10
11
// 生成一份唯一标示
String sn = UUID.randomUUID().toString();

// 创建Provider线程
Provider provider = new Provider(sn);
provider.start();

// 读取任意键盘信息后可以退出
System.in.read();

provider.exit();

Provider线程基于内部类实现

1
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
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
private static class Provider extends Thread {

// 唯一标识符
private final String sn;
// 是否完成标识
private boolean done = false;
// 接收UDP信息实体
private DatagramSocket ds = null;

public Provider(String sn) {
super();
this.sn = sn;
}

@Override
public void run() {
super.run();
System.out.println("UDPProvider Started.");

try {
// 构建UDP监听,监听20000 端口
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();
int port = receivePack.getPort();
int dataLen = receivePack.getLength();
String data = new String(receivePack.getData(), 0, dataLen);
System.out.println("UDPProvider receive form ip:" + ip
+ "\tport:" + port + "\tdata:" + data);

// 解析端口号
int responsePort = MessageCreator.parsePort(data);

if (responsePort != -1) {
// 构建一份回送数据
String responseData = MessageCreator.buildWithSn(sn);
byte[] responseDataBytes = responseData.getBytes();
// 直接根据发送者构建一份回送信息
DatagramPacket responsePacket = new DatagramPacket(responseDataBytes,
responseDataBytes.length,
receivePack.getAddress(),
responsePort);

ds.send(responsePacket);
}
}
} catch (Exception ignored) {
} finally {
close();
}

// 完成
System.out.println("UDPProvider Finished.");

}

private void close() {
if (ds != null) {
ds.close();
ds = null;
}
}

// 提供结束
void exit() {
done = true;
close();
}
}

2. UDPSearcher实现

UDPSearcher类启动

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
// Searcher持续监听端口,Provider回送端口
private static final int LISTEN_PORT = 30000;

// 创建Listener线程,开始监听Port:30000
Listener listener = listen();

// 发送广播
sendBroadcast();

// 读取任意键盘信息后可以退出
System.in.read();

// 得到监听到的设备的信息
List<Device> devices = listener.getDevicesAndClose();
for (Device device : devices) {
System.out.println("Device:" + device.toString());
}

listen()方法实现

1
2
3
4
5
6
7
8
9
private static Listener listen() throws InterruptedException {

System.out.println("UDPSearcher start listen.");
CountDownLatch countDownLatch = new CountDownLatch(1);
Listener listener = new Listener(LISTEN_PORT,countDownLatch);
listener.start();
countDownLatch.await();
return listener;
}

sendBroadcast()广播方法实现

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
private static void sendBroadcast() throws IOException {
System.out.println("UDPSearcher sendBroadcast started.");

// 作为搜索方,系统自动分配端口
DatagramSocket ds = new DatagramSocket();

//构建一份请求数据
String requestData = MessageCreator.buildWithPort(LISTEN_PORT);
byte[] requestDataBytes = requestData.getBytes();

// 直接根据发送者构建一份请求信息
DatagramPacket requestPacket = new DatagramPacket(requestDataBytes,requestDataBytes.length);

// 设置IP地址为广播IP地址,端口还是20000
requestPacket.setAddress(InetAddress.getByName("255.255.255.255"));
requestPacket.setPort(20000);
ds.send(requestPacket);
ds.close();
System.out.println("UDPSearcher sendBroadcast finished.");
}

Listener线程基于内部类实现

1
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
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
private static class Listener extends Thread {

private final int listenPort;
private boolean done = false;
private final CountDownLatch countDownLatch;
private final List<Device> devices = new ArrayList<>();
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();
int port = receivePack.getPort();
int dataLen = receivePack.getLength();
String data = new String(receivePack.getData(),0,dataLen);
System.out.println("UDPProvider receive from ip:"+ip + "\tPort:"+port + "\tdata:"+data);

// 解析操作
String sn = MessageCreator.parseSN(data);
if(sn!=null){
Device device = new Device(port ,ip ,sn);
devices.add(device);
}

}
}catch (Exception ignored ){
}finally {
close();
}

System.out.println("UDPProvider listener Finished");
}

private void close(){
if(ds!=null){
ds.close();
ds = null;
}
}

List<Device> getDevicesAndClose(){
done = true;
close();
return devices;
}
}

Device类基于内部类实现

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
private static class Device{
final int port;
final String ip;
final String sn;

private 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 + '\'' +
'}';
}
}

完整代码SocketDemo_UDP_02

Author: TheOutsider
Link: http://yoursite.com/2020/04/21/Socket%E7%BC%96%E7%A8%8B-UDP/
Copyright Notice: All articles in this blog are licensed under CC BY-NC-SA 4.0 unless stating additionally.