Java 網絡編程(2):UDP 的使用

今天的主角是 UDP(User Datagram Protocol,用戶數據報協議)。
咱們都知道 TCP 是一種可靠的協議 —— 首先客戶端和服務端須要創建鏈接(三次「握手」),數據發送完畢須要斷開鏈接(四次「揮手」);若是發送數據時數據損壞或者丟失,那麼 TCP 會從新發送。保證可靠的代價就是效率的下降(創建鏈接和斷開鏈接就須要時間,保證數據的可靠性也須要額外的消耗)。與 TCP 相對應,UDP 是面向無鏈接的協議,而且它不保證數據是否會到達,也不保證到達的數據是否準確和數據順序是否正確 —— 因此相比於 TCP, UDP 的速度很快。在 須要不創建鏈接便可發送數據 的系統,或者 保證最快的傳輸速度比每一位數據都正確更重要 的系統(如視頻會議,丟失某個數據包只是一個畫面或者聲音的小干擾)中,UDP 纔是正確的選擇。實際上,在同一個網段,或者在信號很好的局域網,UDP 是很是可靠的。java

在 Java 中使用 UDP 時關鍵的兩個類分別是:DatagramSocketDatagramPacket
DatagramPacket 表示數據包,而 DatagrameSocket 用來發送和接收數據包。數組

  • 發送數據包時:

由於 UDP 協議並不須要創建鏈接,因此咱們將數據(byte數組)放入 DatagramPacket 以後,還須要將目的地(IP地址和端口)放入到 DatagramPacket 中 —— DatagramSocket 的 send(DatagramPacket packet) 方法根據 packet 中指定目的地,將其包含的數據往這個目的地發送。至於數據是否能(準確)到達目的地,DatagramSocket 並不關心。socket

  • 接收數據包時:

DatagramSocket 在接收數據包時,咱們須要爲其指定一個監聽的端口。當有包含了接收端機器 IP 地址和 DatagramSocket 所監聽端口的數據包到達時,DatagramSocket 的 receive(DatagramPacket packet) 方法便會對數據包進行接收,並將接收到的數據包填入到 packet 中。阿里雲

如今讓咱們來實踐 DatagramSocket 和 DatagramPacket:
由於 UDP 沒有服務端和客戶端之分,因此咱們把兩端分別定義爲 發送端 和 接收端。
發送端代碼:spa

import java.io.IOException;
import java.net.DatagramPacket;
import java.net.DatagramSocket;
import java.net.InetAddress;
import java.net.UnknownHostException;
import java.util.ArrayList;
import java.util.Collections;
import java.util.List;

public class UDPSender {

    private static final int RECVER_LISTENING_PORT = 9999;

    public static void main(String[] args) throws Exception {
        List<DatagramPacket> messages = new ArrayList<>(12);
        for (int i = 0; i < 4; i++) {
            String msgBody = (i == 3 ? "" : "Hello UDP-" + i);

            DatagramPacket msg0 = parseMsg(
                    msgBody, "127.0.0.1", RECVER_LISTENING_PORT); // 發送給本機
            DatagramPacket msg1 = parseMsg(
                    msgBody, "192.168.3.3", RECVER_LISTENING_PORT); // 發送給同一局域網的一臺機器
            DatagramPacket msg2 = parseMsg(
                    msgBody, "120.77.**.***", RECVER_LISTENING_PORT); // 120.77.**.*** 是我阿里雲主機的公網 IP 地址

            // JDK1.5 時 Collections 添加的 addAll 方法,能夠一次往某個集合中添加多個元素
            Collections.addAll(messages, msg0, msg2, msg1);
        }

        startSending(messages);
    }

    private static void startSending(List<DatagramPacket> messages)
            throws IOException, InterruptedException {

        // 無參構造的 DatagramSocket 會隨機選擇一個端口進行監聽
        // 由於此時 DatagramSocket 的做用是發送,因此無需顯式指定固定端口
        try (DatagramSocket socket = new DatagramSocket()) {
            System.out.println("隨機給發送端分配的端口爲:" + socket.getLocalPort() + "\n");
            
            for (DatagramPacket msg : messages) {
                socket.send(msg); // 發送數據包

                int recverPort = msg.getPort();
                InetAddress recverAddr = msg.getAddress();
                System.out.printf("消息已經發送 -> (%s:%d)\n",
                        recverAddr.getHostAddress(), recverPort);

                Thread.sleep(500); // 設定 每隔 0.5 秒發送一個消息
            }
        }
    }

    private static DatagramPacket parseMsg(String msgBody, String addr, int port)
            throws UnknownHostException {

        byte[] msgData = msgBody.getBytes();
        DatagramPacket msg = new DatagramPacket(
                msgData, 0, msgData.length, // 數據從位置 0 開始,長度爲 msgData.length
                InetAddress.getByName(addr), port); // 目的地 地址爲 addr,監聽端口爲 port

        return msg;
    }

}

接收端代碼:.net

import java.net.DatagramPacket;
import java.net.DatagramSocket;
import java.net.InetAddress;

public class UDPReceiver {

    private static final int LISTENING_PORT = 9999;
    private static final int BUFFER_SIZE = 512;

    public static void main(String[] args) throws Exception {
        byte[] buffer = new byte[BUFFER_SIZE];
        DatagramPacket msg = new DatagramPacket(buffer, buffer.length);

        try (DatagramSocket socket = new DatagramSocket(LISTENING_PORT)) {
            System.out.println("接收端已經啓動...\n");
            while (true) {
                socket.receive(msg); // 接收數據包

                String msgBody = new String(
                        msg.getData(), msg.getOffset(), msg.getLength());
                if (msgBody.isEmpty()) { // 發現接收的消息是空字符串("")便跳出循環
                    break;
                }

                int senderPort = msg.getPort();
                InetAddress senderAddr = msg.getAddress();

                System.out.printf("發送端 地址和端口 -> (%s:%d)\n",
                        senderAddr.getHostAddress(), senderPort);

                System.out.println("發送端 發送的消息 -> " + msgBody + "\n");
            }
        }

        System.out.println("接收端已經關閉。");
    }
}

如今 在本機、同一局域網的一臺機器和阿里雲主機上都運行 UDPReceiver:
本機接收端啓動code

局域網機器接收端啓動

阿里雲主機接收端啓動

而後啓動發送端 UDPSender:
發送端發送數據視頻

接收端接收結果:
本機接收結果開發

局域網機器接收結果

阿里雲主機接收結果

能夠看到每一個接收端都正確的接收了發送端發送的消息。字符串


(本機 和 局域網機器 顯示的發送端的端口(49756)是一致的,可是遠程機器即阿里雲主機顯示的(4802)卻與他們不一致——這個問題留給有興趣的讀者本身思考)


雖然 UDP 是不可靠的協議,可是由於它不須要創建鏈接,效率更快,因此在 須要不創建鏈接便可發送數據 的系統(好比本文的例子),或者 保證最快的傳輸速度比每一位數據都正確更重要 的系統中,咱們應該使用 UDP。固然,基於 UDP 咱們一樣能夠開發出可靠的協議——數據包的正確與否能夠交給應用程序來判斷,若是有問題接收端便提示發送端從新發送。

相關文章
相關標籤/搜索