Java UDP Socket 數據收發
1. 前言
UDP 是面向數據報的傳輸協議。UDP 的包頭非常簡單,總共占用 8 字節長度,格式如下:
+--------------------+--------------------+
| 源端口(16 bits) | 目的端口(16 bits) |
+--------------------+--------------------+
| 包的長度(16 bits) | 檢驗和(16 bits) |
+--------------------+--------------------+
源端口號:占用 2 字節長度,用于標識發送端應用程序。
目的端口:占用 2 字節長度,用于標識接收端應用程序。
包的長度:表示 UDP 數據包的總長度,占用 2 字節長度。包的長度的值是 UDP 包頭的長度加上 UDP 包體的長度。 包體最大長度是 65536-8 = 65528 字節。
提示:
網絡層的 IPv4 Header 也包含了 Length 字段,IPv4 Payload 的最大長度是 65536-60 = 65476 字節。如果我們控制 UDP 數據包總長度不超過 65476 字節,UDP Header 其實是不需要 UDP Length 字段的。因為在實際開發中,程序員會保證傳給 UDP 的數據長度不超過 MTU 最大限度,所以 UDP Length 字段顯得有點兒多余。
檢驗和:占用 2 字節長度。UDP 檢驗和是用于差錯檢測的,檢驗計算包含 UDP 包頭和 UDP 包體兩部分。
從 UDP 的協議格式可以看出,UDP 保證了應用層消息的完整性,比如,UDP 客戶端向服務器發送 “Hello Server,I’m client。How are you?”,UDP 客戶端發送的是具有一定含義的數據,UDP 服務端收到也是這個完整的消息。不像面向字節流的 TCP 協議,需要應用程序解析消息。為此,UDP 編程會簡單很多。
2. Java DatagramPacket 分析
Java 抽象了 java.net.DatagramPacket 類表示一個 UDP 數據報,主要功能如下:
- 發送:
- 設置發送的數據。
- 設置接收此數據的目的主機的 IP 地址和端口號。
- 獲取發送此數據的源主機的 IP 地址和端口號。
- 接收:
- 設置接收數據的 byte 數組。
- 獲取發送此數據的源主機的 IP 地址和端口號。
- 獲取接收此數據的主機目的主機的 IP 地址和端口號。
接收數據的構造方法:
public DatagramPacket(byte[] buffer, int length)
public DatagramPacket(byte[] buffer, int offset, int length)
當接收數據的時候,需要構造 java.net.DatagramPacket 的實例,并且要傳入接收數據的 byte 數組,然后調用 java.net.DatagramSocket 的 receive 方法就可以接收數據。當 receive 方法調用返回以后,發送此數據包的源主機 IP 地址和端口號保存在 java.net.DatagramSocket 的實例中。
發送數據的構造方法:
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)
當發送數據的時候,同樣需要構造 java.net.DatagramPacket 的實例,并且要傳入將要發送的數據的 byte 數組,同時要傳入接收此數據包的目標主機 IP 地址和端口號,然后調用 java.net.DatagramSocket 的 send 方法就可以發送數據。目標主機的 IP 地址和端口號保存在 java.net.DatagramSocket 的實例中,你可以調用它的 getSocketAddress 方法獲取。
獲取或設置數據:
public byte[] getData()
public void setData(byte[] data)
public void setData(byte[] data, int offset, int length)
獲取或設置數據的長度:
public int getLength()
public void setLength(int length)
獲取設置 IP 地址和端口號
public int getPort()
public InetAddress getAddress() // 只能獲取 IP
public SocketAddress getSocketAddress()// 同時獲取 IP 和 Port
public void setAddress(InetAddress remote)// 只能設置 IP
public void setPort(int port)
public void setAddress(SocketAddress remote)// 設置 SocketAddress,同時設置 IP 和 Port
3. UDP 消息序列化與反序列化
java.io.ByteArrayInputStream 和 java.io.ByteArrayOutputStream 繼承自 java.io.InputStream 和 java.io.OutputStream??梢宰鳛榱鞯脑春土鞯哪繕祟?,當你需要解析復雜的協議格式的時候,可以配合 java.io.DataInputStream 和 java.io.DataOutputStream 類實現消息的序列化、反序列化。
下來我們定義一個簡單的消息格式,通常在音視頻通信中會遇到這樣的消息格式,我們這里只是一個演示版本。具體字段如下:
- version 表示協議版本號,這是一般協議格式都會包含的一個字段。
- flag,一些控制標志,主要表現在用不同的 bit 位表示不同的控制標志。
- sequence,對每個消息進行編號,用來檢測是否有丟包發生。
- timestamp,每一個消息都攜帶一個發送時間戳,可以計算網絡延遲、抖動。
- 消息體,消息的具體內容。
圖示如下:
+-----------------+-----------------+-----------------|-----------------+
| version(8 bits) | flag(8 bits) | Sequence(16 bits) |
+-----------------|-----------------+-----------------------------------+
| Timestamp(32 bits) |
+++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++
| |
| Message Body |
| |
+++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++
對于這樣一個格式,通過 java.net.DatagramPacket 類讀取或者是設置的是一個 byte 數組,要想解析數組中消息各個字段的含義,需要借助 java.io.ByteArrayInputStream 和 java.io.ByteArrayOutputStream 類,以及 java.io.DataInputStream 和 java.io.DataOutputStream 類。
我們設計了一個 Message 類用來表示消息結構,當然 Message 類要包含協議格式中的各個字段。除了提供各個屬性的 getter/setter 方法外,還提供了 serialize 和 deserialize 方法,實現了消息的序列化、反序列化。最后,我們覆蓋了 toString 方,將 Message 轉換成 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);
// 構造發送數據包,需要傳入消息內容和目標地址結構 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 的配合,實現 serialize 功能。通過 DataInputStream 和 ByteArrayInputStream 配合,實現 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 是一個 DatagramPacket 類型的實例
4. 小結
本節首先介紹了 java.net.DatagramPacket 類的基本功能,這是 Java UDP Socket 程序進行數據讀寫的基礎類。在調用 receive 方法接收數據之前,首先要創建 DatagramPacket 的實例,同時要為他提供一個介紹數據的字節數組。當 receive 方法成功返回后,你可以調用 DatagramPacket 的 getSocketAddress 方法獲取發送主機的源 IP 地址和端口號。在調用 send 方法發送數據之前,首先要創建 DatagramPacket 的實例,將要發送的數據傳給他,同時要將接收數據的目標主機的 IP 地址和端口號設置給它。
接著我們重點介紹了 UDP 編程中常見的協議格式定義、解析方法,主要是通過 java.io.ByteArrayInputStream 和 java.io.ByteArrayOutputStream 類,以及 java.io.DataInputStream 和 java.io.DataOutputStream 類實現消息的序列化、反序列化功能。我們提供了完整的實現代碼,已經序列化、反序列化的具體用法??梢哉f這一部分內容在實踐中經常會遇到,需要好好掌握。