Java UDP Socket 數(shù)據(jù)收發(fā)
1. 前言
UDP 是面向數(shù)據(jù)報(bào)的傳輸協(xié)議。UDP 的包頭非常簡單,總共占用 8 字節(jié)長度,格式如下:
+--------------------+--------------------+
| 源端口(16 bits) | 目的端口(16 bits) |
+--------------------+--------------------+
| 包的長度(16 bits) | 檢驗(yàn)和(16 bits) |
+--------------------+--------------------+
源端口號:占用 2 字節(jié)長度,用于標(biāo)識發(fā)送端應(yīng)用程序。
目的端口:占用 2 字節(jié)長度,用于標(biāo)識接收端應(yīng)用程序。
包的長度:表示 UDP 數(shù)據(jù)包的總長度,占用 2 字節(jié)長度。包的長度的值是 UDP 包頭的長度加上 UDP 包體的長度。 包體最大長度是 65536-8 = 65528 字節(jié)。
提示:
網(wǎng)絡(luò)層的 IPv4 Header 也包含了 Length 字段,IPv4 Payload 的最大長度是 65536-60 = 65476 字節(jié)。如果我們控制 UDP 數(shù)據(jù)包總長度不超過 65476 字節(jié),UDP Header 其實(shí)是不需要 UDP Length 字段的。因?yàn)樵趯?shí)際開發(fā)中,程序員會保證傳給 UDP 的數(shù)據(jù)長度不超過 MTU 最大限度,所以 UDP Length 字段顯得有點(diǎn)兒多余。
檢驗(yàn)和:占用 2 字節(jié)長度。UDP 檢驗(yàn)和是用于差錯(cuò)檢測的,檢驗(yàn)計(jì)算包含 UDP 包頭和 UDP 包體兩部分。
從 UDP 的協(xié)議格式可以看出,UDP 保證了應(yīng)用層消息的完整性,比如,UDP 客戶端向服務(wù)器發(fā)送 “Hello Server,I’m client。How are you?”,UDP 客戶端發(fā)送的是具有一定含義的數(shù)據(jù),UDP 服務(wù)端收到也是這個(gè)完整的消息。不像面向字節(jié)流的 TCP 協(xié)議,需要應(yīng)用程序解析消息。為此,UDP 編程會簡單很多。
2. Java DatagramPacket 分析
Java 抽象了 java.net.DatagramPacket 類表示一個(gè) UDP 數(shù)據(jù)報(bào),主要功能如下:
- 發(fā)送:
- 設(shè)置發(fā)送的數(shù)據(jù)。
- 設(shè)置接收此數(shù)據(jù)的目的主機(jī)的 IP 地址和端口號。
- 獲取發(fā)送此數(shù)據(jù)的源主機(jī)的 IP 地址和端口號。
- 接收:
- 設(shè)置接收數(shù)據(jù)的 byte 數(shù)組。
- 獲取發(fā)送此數(shù)據(jù)的源主機(jī)的 IP 地址和端口號。
- 獲取接收此數(shù)據(jù)的主機(jī)目的主機(jī)的 IP 地址和端口號。
接收數(shù)據(jù)的構(gòu)造方法:
public DatagramPacket(byte[] buffer, int length)
public DatagramPacket(byte[] buffer, int offset, int length)
當(dāng)接收數(shù)據(jù)的時(shí)候,需要構(gòu)造 java.net.DatagramPacket 的實(shí)例,并且要傳入接收數(shù)據(jù)的 byte 數(shù)組,然后調(diào)用 java.net.DatagramSocket 的 receive 方法就可以接收數(shù)據(jù)。當(dāng) receive 方法調(diào)用返回以后,發(fā)送此數(shù)據(jù)包的源主機(jī) IP 地址和端口號保存在 java.net.DatagramSocket 的實(shí)例中。
發(fā)送數(shù)據(jù)的構(gòu)造方法:
public DatagramPacket(byte[] data, int length,InetAddress destination, int port)
public DatagramPacket(byte[] data, int offset, int length,InetAddress destination, int port)
public DatagramPacket(byte[] data, int length,SocketAddress destination)
public DatagramPacket(byte[] data, int offset, int length,SocketAddress destination)
當(dāng)發(fā)送數(shù)據(jù)的時(shí)候,同樣需要構(gòu)造 java.net.DatagramPacket 的實(shí)例,并且要傳入將要發(fā)送的數(shù)據(jù)的 byte 數(shù)組,同時(shí)要傳入接收此數(shù)據(jù)包的目標(biāo)主機(jī) IP 地址和端口號,然后調(diào)用 java.net.DatagramSocket 的 send 方法就可以發(fā)送數(shù)據(jù)。目標(biāo)主機(jī)的 IP 地址和端口號保存在 java.net.DatagramSocket 的實(shí)例中,你可以調(diào)用它的 getSocketAddress 方法獲取。
獲取或設(shè)置數(shù)據(jù):
public byte[] getData()
public void setData(byte[] data)
public void setData(byte[] data, int offset, int length)
獲取或設(shè)置數(shù)據(jù)的長度:
public int getLength()
public void setLength(int length)
獲取設(shè)置 IP 地址和端口號
public int getPort()
public InetAddress getAddress() // 只能獲取 IP
public SocketAddress getSocketAddress()// 同時(shí)獲取 IP 和 Port
public void setAddress(InetAddress remote)// 只能設(shè)置 IP
public void setPort(int port)
public void setAddress(SocketAddress remote)// 設(shè)置 SocketAddress,同時(shí)設(shè)置 IP 和 Port
3. UDP 消息序列化與反序列化
java.io.ByteArrayInputStream 和 java.io.ByteArrayOutputStream 繼承自 java.io.InputStream 和 java.io.OutputStream。可以作為流的源和流的目標(biāo)類,當(dāng)你需要解析復(fù)雜的協(xié)議格式的時(shí)候,可以配合 java.io.DataInputStream 和 java.io.DataOutputStream 類實(shí)現(xiàn)消息的序列化、反序列化。
下來我們定義一個(gè)簡單的消息格式,通常在音視頻通信中會遇到這樣的消息格式,我們這里只是一個(gè)演示版本。具體字段如下:
- version 表示協(xié)議版本號,這是一般協(xié)議格式都會包含的一個(gè)字段。
- flag,一些控制標(biāo)志,主要表現(xiàn)在用不同的 bit 位表示不同的控制標(biāo)志。
- sequence,對每個(gè)消息進(jìn)行編號,用來檢測是否有丟包發(fā)生。
- timestamp,每一個(gè)消息都攜帶一個(gè)發(fā)送時(shí)間戳,可以計(jì)算網(wǎng)絡(luò)延遲、抖動。
- 消息體,消息的具體內(nèi)容。
圖示如下:
+-----------------+-----------------+-----------------|-----------------+
| version(8 bits) | flag(8 bits) | Sequence(16 bits) |
+-----------------|-----------------+-----------------------------------+
| Timestamp(32 bits) |
+++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++
| |
| Message Body |
| |
+++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++
對于這樣一個(gè)格式,通過 java.net.DatagramPacket 類讀取或者是設(shè)置的是一個(gè) byte 數(shù)組,要想解析數(shù)組中消息各個(gè)字段的含義,需要借助 java.io.ByteArrayInputStream 和 java.io.ByteArrayOutputStream 類,以及 java.io.DataInputStream 和 java.io.DataOutputStream 類。
我們設(shè)計(jì)了一個(gè) Message 類用來表示消息結(jié)構(gòu),當(dāng)然 Message 類要包含協(xié)議格式中的各個(gè)字段。除了提供各個(gè)屬性的 getter/setter 方法外,還提供了 serialize 和 deserialize 方法,實(shí)現(xiàn)了消息的序列化、反序列化。最后,我們覆蓋了 toString 方,將 Message 轉(zhuǎn)換成 String 形式。
代碼清單如下:
import java.io.*;
import java.net.DatagramPacket;
public class Message implements Serializable {
private static final int SEND_BUFF_LEN = 512;
private static ByteArrayOutputStream outArray = new ByteArrayOutputStream(SEND_BUFF_LEN);
private byte version =1;
private byte flag;
private short sequence;
private int timestamp;
private byte[] body = null;
private int bodyLength = 0;
public byte getVersion() {
return version;
}
public void setVersion(byte version) {
this.version = version;
}
public byte getFlag() {
return flag;
}
public void setFlag(byte flag) {
this.flag = flag;
}
public short getSequence() {
return sequence;
}
public void setSequence(short sequence) {
this.sequence = sequence;
}
public int getTimestamp() {
return timestamp;
}
public void setTimestamp(int timestamp) {
this.timestamp = timestamp;
}
public byte[] getBody() {
return body;
}
public void setBody(byte[] body) {
this.body = body;
}
public DatagramPacket serialize()
{
try {
outArray.reset();
DataOutputStream out = new DataOutputStream(outArray);
out.writeByte(this.getVersion());
out.writeByte(this.getFlag());
out.writeShort(this.getSequence());
out.writeInt(this.getTimestamp());
out.write(this.body);
// 構(gòu)造發(fā)送數(shù)據(jù)包,需要傳入消息內(nèi)容和目標(biāo)地址結(jié)構(gòu) SocketAddress
byte[] outBytes = outArray.toByteArray();
DatagramPacket message = new DatagramPacket(outBytes, outBytes.length);
return message;
} catch (IOException e) {
e.printStackTrace();
}
return null;
}
public void deserialize(DatagramPacket inMessage)
{
try {
DataInputStream in = new DataInputStream(
new ByteArrayInputStream(inMessage.getData(), 0, inMessage.getLength()));
this.version = in.readByte();
this.flag = in.readByte();
this.sequence = in.readShort();
this.timestamp = in.readInt();
this.body = new byte[512];
this.bodyLength = in.read(this.body);
} catch (IOException e) {
e.printStackTrace();
}
}
@Override
public String toString() {
return " version: " + this.getVersion()
+ " flag: " + this.getFlag()
+ " sequence: " + this.getSequence()
+ " timestamp: " + this.getSequence()
+ " message body: " +new String(body, 0, this.bodyLength);
}
}
通過 DataOutputStream 和 ByteArrayOutputStream 的配合,實(shí)現(xiàn) serialize 功能。通過 DataInputStream 和 ByteArrayInputStream 配合,實(shí)現(xiàn) deserialize 功能。
Message 序列化的用法:
private static final int PORT = 9002;
private static final String DST_HOST = "127.0.0.1";
private static short sequence = 1;
SocketAddress to = new InetSocketAddress(DST_HOST, PORT);
String req = "Hello Server!";
Message sMsg = new Message();
sMsg.setVersion((byte)1);
sMsg.setFlag((byte)21);
sMsg.setSequence(sequence++);
sMsg.setTimestamp((int)System.currentTimeMillis()&0xFFFFFFFF);
sMsg.setBody(req.getBytes());
DatagramPacket outMessage = sMsg.serialize();
outMessage.setSocketAddress(to);
Message 反列化的用法:
Message rMsg = new Message();
rMsg.deserialize(inMessage);// inMessage 是一個(gè) DatagramPacket 類型的實(shí)例
4. 小結(jié)
本節(jié)首先介紹了 java.net.DatagramPacket 類的基本功能,這是 Java UDP Socket 程序進(jìn)行數(shù)據(jù)讀寫的基礎(chǔ)類。在調(diào)用 receive 方法接收數(shù)據(jù)之前,首先要創(chuàng)建 DatagramPacket 的實(shí)例,同時(shí)要為他提供一個(gè)介紹數(shù)據(jù)的字節(jié)數(shù)組。當(dāng) receive 方法成功返回后,你可以調(diào)用 DatagramPacket 的 getSocketAddress 方法獲取發(fā)送主機(jī)的源 IP 地址和端口號。在調(diào)用 send 方法發(fā)送數(shù)據(jù)之前,首先要創(chuàng)建 DatagramPacket 的實(shí)例,將要發(fā)送的數(shù)據(jù)傳給他,同時(shí)要將接收數(shù)據(jù)的目標(biāo)主機(jī)的 IP 地址和端口號設(shè)置給它。
接著我們重點(diǎn)介紹了 UDP 編程中常見的協(xié)議格式定義、解析方法,主要是通過 java.io.ByteArrayInputStream 和 java.io.ByteArrayOutputStream 類,以及 java.io.DataInputStream 和 java.io.DataOutputStream 類實(shí)現(xiàn)消息的序列化、反序列化功能。我們提供了完整的實(shí)現(xiàn)代碼,已經(jīng)序列化、反序列化的具體用法??梢哉f這一部分內(nèi)容在實(shí)踐中經(jīng)常會遇到,需要好好掌握。