亚洲在线久爱草,狠狠天天香蕉网,天天搞日日干久草,伊人亚洲日本欧美

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這一部分內容在實踐中經常會遇到,需要好好掌握。