Socket網絡編程(二):Socket UDP快速入門

博客主頁java

UDP是Socket中重要組成部分,下面主要想帶你們一塊兒瞭解什麼是UDP,以及UDP能夠用來作什麼。編程

1. UDP是什麼?

UDP全稱爲User Datagram Protocol,縮寫爲UDP,稱爲用戶數據報協議,也叫用戶數據報文協議。它是一個簡單的面向數據報的傳輸層協議,正式規範爲RFC 768。在上一篇Socket網絡編程理論知識中介紹了UDP是一種面向無鏈接的協議,所以,在通訊時發送端和接收端不用創建鏈接。segmentfault

UDP通訊的過程就像是貨運公司在兩個碼頭間發送貨物同樣,在碼頭髮送和接收貨物時都須要使用集裝箱來裝載貨物,UDP通訊也是同樣,發送和接收的數據也須要使用「集裝箱」進行打包。數組

UDP爲何不可靠呢?緩存

  1. 它一旦把應用程序發給網絡層的數據發送出去,就不保留數據備份
  2. UDP在IP數據報的頭部僅僅加入了複用和數據校驗
  3. 發送端生產數據,接收端從網絡中抓取數據
  4. 結構簡單,無檢驗,速度快,容易丟包,可廣播

2. 基於UDP協議的能作什麼呢?

2.1 網頁或者APP的訪問

原來訪問網頁和手機APP都是基於HTTP協議的。HTTP協議是基於TCP的,創建鏈接都須要屢次交互,對於時延比較大的目前主流的移動互聯網來說,創建一次鏈接須要的時間會比較長,然而既然是移動中,TCP可能還會斷了重連,也是很耗時的。並且目前的HTTP協議,每每採起多個數據通道共享一個鏈接的狀況,這樣原本爲了加快傳輸速度,可是TCP的嚴格順序策略使得哪怕共享通道,前一個不來,後一個和前一個即使不要緊,也要等着,時延也會加大。服務器

而QUIC(全稱Quick UDP Internet Connections,快速UDP互聯網鏈接)是Google提出的一種基於UDP改進的通訊協議,其目的是下降網絡通訊的延遲,提供更好的用戶互動體驗。網絡

QUIC在應用層上,會本身實現快速鏈接創建、減小重傳時延,自適應擁塞控制,是應用層「城會玩」的表明。這一節主要是講UDP,QUIC咱們放到應用層去講。數據結構

2.2 流媒體的協議

如今直播比較火,直播協議多使用RTMP,這個協議咱們後面的章節也會講,而這個RTMP協議也是基於TCP的。TCP的嚴格順序傳輸要保證前一個收到了,下一個才能確認,若是前一個收不到,下一個就算包已經收到了,在緩存裏面,也須要等着。對於直播來說,這顯然是不合適的,由於老的視頻幀丟了其實也就丟了,就算再傳過來用戶也不在乎了,他們要看新的了,若是總是沒來就等着,卡頓了,新的也看不了,那就會丟失客戶,因此直播,實時性比較比較重要,寧肯丟包,也不要卡頓的。dom

另外,對於丟包,其實對於視頻播放來說,有的包能夠丟,有的包不能丟,由於視頻的連續幀裏面,有的幀重要,有的不重要,若是必需要丟包,隔幾個幀丟一個,其實看視頻的人不會感知,可是若是連續丟幀,就會感知了,於是在網絡很差的狀況下,應用但願選擇性的丟幀。異步

還有就是當網絡很差的時候,TCP協議會主動下降發送速度,這對原本當時就卡的看視頻來說是要命的,應該應用層立刻重傳,而不是主動讓步。於是,不少直播應用,都基於UDP實現了本身的視頻傳輸協議。

2.3 實時遊戲

遊戲有一個特色,就是實時性比較高。

實時遊戲中客戶端和服務端要創建長鏈接,來保證明時傳輸。可是遊戲玩家不少,服務器卻很少。因爲維護TCP鏈接須要在內核維護一些數據結構,於是一臺機器可以支撐的TCP鏈接數目是有限的,而後UDP因爲是沒有鏈接的,在異步IO機制引入以前,經常是應對海量客戶端鏈接的策略。

另外仍是TCP的強順序問題,對戰的遊戲,對網絡的要求很簡單,玩家經過客戶端發送給服務器鼠標和鍵盤行走的位置,服務器會處理每一個用戶發送過來的全部場景,處理完再返回給客戶端,客戶端解析響應,渲染最新的場景展現給玩家。

若是出現一個數據包丟失,全部事情都須要停下來等待這個數據包重發。客戶端會出現等待接收數據,然而玩家並不關心過時的數據,激戰中卡1秒,等能動了都已經死了。

遊戲對實時要求較爲嚴格的狀況下,採用自定義的可靠UDP協議,自定義重傳策略,可以把丟包產生的延遲降到最低,儘可能減小網絡問題對遊戲性形成的影響。

2.4 IoT物聯網

一方面,物聯網領域終端資源少,極可能只是個內存很是小的嵌入式系統,而維護TCP協議代價太大;另外一方面,物聯網對實時性要求也很高,而TCP時延大。Google旗下的Nest創建Thread Group,推出了物聯網通訊協議Thread,就是基於UDP協議的。

2.5 移動通訊領域

在4G網絡裏,移動流量上網的數據面對的協議GTP-U是基於UDP的。由於移動網絡協議比較複雜,而GTP協議自己就包含複雜的手機上線下線的通訊協議。若是基於TCP,TCP的機制就顯得很是多餘。

3. UDP核心API

在UDP通訊中有2個經常使用的類:一個是數據包類DatagramPacket,一個是數據包發送接收器類DatagramSocket

根據API文檔的內容,對UDP兩個經常使用類進行分析:

3.1 DatagramPacket

在java中,提供了一個DatagramPacket類,該類的實例對象就至關於一個集裝箱,用來封裝UDP通訊中發送或者接收的數據。

首先須要瞭解下DatagramPacket的構造方法。在建立發送端和接收端的DatagramPacket對象時,使用的構造方法有所不一樣,接收端的構造方法只須要接收一個字節數組來存放接收到的數據,而發送端的構造方法不但要存放發送數據的字節數組,還須要指定發送端的IP地址和端口號。

先來了解下DatagramPacket的構造方法:

  1. 使用該構造方法建立DatagramPacket對象時,指定了封裝數據的字節數組和數據的大小,沒有指定IP地址和端口號。說明只能用於接收端,不能用於發送端,由於發送端必定要明確指出數據的目的地(IP地址和端口號),而接收端不須要明確知道數據的來源,只須要接收便可


  1. 使用該構造方法建立DatagramPacket對象時,不只指定了封裝數據的字節數組和數據大小,還指定了數據包的目標IP地址(address)和端口號(port)。該對象一般用於發送端,由於在發送數據時必須指定接收端的IP地址和端口號,就好像發送貨物的集裝箱上面必須標明接收人的地址同樣。

其中SocketAddress對象封裝了IP地址+端口號,至關於InetAddress+端口號port。

// 從SocketAddress子類的構造方法能夠看出
InetSocketAddress(InetAddress addr, int port)


瞭解了DatagramPacket構造方法,接下來對DatagramPacket類中的經常使用方法進行說明:



3.2 DatagramSocket

DatagramPacket數據包的做用就如同是「集裝箱」,能夠將發送端或者接受端的數據封裝起來。然而運輸貨物只有「集裝箱」是不夠的,還須要有碼頭。在程序中須要實現通訊只有DatagramPacket數據包也一樣不行,爲此JDK中提供一個DatagramSocket類。DatagramSocket類的做用就相似於碼頭,使用這個類的實例對象就能夠發送和接收DatagramPacket數據包。

在建立發送端和接收端的DatagramSocket對象時,使用的構造方法有所不一樣。

先來了解下DatagramSocket構造方法:

  1. 該構造用於建立發送端的DatagramSocket對象,在建立DatagramSocket對象時,並無指定端口號,此時,系統會分配一個沒有被其它網絡程序所使用的端口號。

  1. 該構造既可建立接收端的DatagramSocket對象,又能夠建立發送端的DatagramSocket對象,在建立接收端DatagramSocket對象時,必需要指定一個端口號,就能夠監聽指定的端口。

瞭解了DatagramSocket構造方法,接下來對DatagramSocket類中的經常使用方法進行說明:

  1. receive(DatagramPacket p) 接收數據報包,接收到的數據封裝到DatagramPacket,還包含發送者的IP地址和發件人機器上的端口號。該方法阻塞,直到接收到數據報。
  2. send(DatagramPacket p) 發送數據報包,DatagramPacket包括指示要發送的數據,其長度,遠程主機的IP地址和遠程主機上的端口號的信息。
  3. close() 關閉此數據報套接字。全部當前阻塞的線程在receive(java.net.DatagramPacket)在此套接字將拋出一個SocketException 。

4. 基於UDP協議的Socket程序函數調用過程,實現簡單聊天案例

使用UDP完成一個簡易的聊天程序案例:在發送端控制檯中輸入要發送的消息,接收端接收發送端發來的消息,並在接收端控制檯中輸出發送端的IP地址、端口號和消息,當發送端輸入886,發送端和接收端都結束。

UDP完成接收端程序:

public class UdpReceive {
    public static void main(String[] args) throws IOException {

        // 1. 建立DatagramPacket對象,用於封裝一個字節數組,用於接收數據
        byte[] data = new byte[1024]; // 最大長度1024*64=64KB
        DatagramPacket receiverPacket = new DatagramPacket(data, data.length);

        // 2. 建立DatagramSocket對象,綁定到本地主機上的指定端口
        DatagramSocket socket = new DatagramSocket(10002);

        while (true) {
            // 3. 使用DatagramSocket對象的receive方法,接收數據包
            // 該方法阻塞,直到接收到數據報
            socket.receive(receiverPacket);

            // 4. 拆包
            // 返回該數據報發送或接收數據報的計算機的IP地址。
            String ip = receiverPacket.getAddress().getHostAddress();
            // 返回發送數據報的遠程主機上的端口號,或從中接收數據報的端口號。
            int port = receiverPacket.getPort();
            // 返回要發送的數據的長度或接收到的數據的長度。
            int length = receiverPacket.getLength();

            String message = new String(data, 0, length);

            System.out.println("Receive-> receiver data: " + message + " from " + ip + ":" + port);

            if ("886".equalsIgnoreCase(message)) {
                // 關閉接受者,不在接收消息
                break;
            }
        }

        // 5. 關閉此數據報套接字。
        socket.close();
    }
}

UDP完成發送端程序:

public class UdpSend {
    public static void main(String[] args) throws IOException {

        Scanner scanner = new Scanner(System.in);

        InetAddress address = InetAddress.getByName("127.0.0.1");

        // 2. 建立DatagramSocket對象,系統會分配一個可用的端口號
        DatagramSocket socket = new DatagramSocket();

        while (true) {
            String message = scanner.nextLine(); // 讀取輸入的數據
            byte[] data = message.getBytes();

            // 1.建立DatagramPacket對象,用於封裝長度爲length數據報包發送到指定主機上的指定端口號。
            DatagramPacket sendPacket = new DatagramPacket(data, data.length, address, 10002);

            // 3.使用DatagramSocket對象中的send方法,發送數據報包
            socket.send(sendPacket);

            if ("886".equalsIgnoreCase(message)) {
                // 結束聊天
                break;
            }
        }

        // 4. 關閉此數據報套接字
        socket.close();
    }
}

5.IP地址和端口號

5.1 IP地址

IP地址是指互聯網協議地址(Internet Protocol Address)。IP地址用來給一個網絡中的計算機設備作一個惟一的編號。在TCP/IP協議中,這個標識號就是IP地址。目前普遍使用的IP地址是IPv4。

IP地址分類:

  1. IPv4

它由4個字節大小的二進制數表示,如:00001010000100000010100100000001。因爲二進制形式表示的IP地址很是不便記憶和處理,所以一般會將IP地址寫成十進制的形式,每一個字節用一個十進制數字(0-255)表示,數字間用符號「.」分開,如 「192.168.1.100」。

  1. IPv6

隨着計算機網絡規模的不斷擴大,對IP地址的需求也愈來愈多,IPV4這種用4個字節表示的IP地址面臨枯竭,所以IPv6 便應運而生了,IPv6採用128位地址長度,每16個字節一組,分紅8組十六進制數,表示成fd00:EF01:4023:6507:bb92:e153:ef13:6789。

InetAddress
JDK中提供了一個InetAddress類,該類用於封裝一個IP地址,並提供了一系列與IP地址相關的方法:

public class InetAddressDemo {
    public static void main(String[] args) throws UnknownHostException {
        // 返回本地主機的地址
        InetAddress local = InetAddress.getLocalHost();
        System.out.println("本機的IP地址:" + local.getHostAddress()); // 172.20.43.73
        System.out.println("本機IP地址的主機名:" + local.getHostName()); // YQBMAC-0050

        //肯定主機名稱的IP地址。
        InetAddress remote = InetAddress.getByName("218.98.31.235");
        System.out.println("remote的IP地址:" + remote.getHostAddress()); // 218.98.31.235
        System.out.println("remote的主機名:" + local.getHostName()); // YQBMAC-0050

    }
}

IP地址類別:

從上圖可知,不一樣的類別能夠經過子網掩碼來區分。咱們經常使用的是B類和C類地址。
先來看下UDP的廣播和多播相關知識:

  1. 255.255.255.255爲受限的廣播地址,即全部網段都能收到,但路由並不會去轉發該廣播,畢竟全部網段都會接受,因此只有本局域網可以接收到。
  2. X.X.X.255 爲 C 類廣播,只有該網段下的才能收到 ,好比 192.168.33.255,那麼 192.168.33.X 下的全部網段都能接收到。
  3. D類IP地址爲多播預留。
廣播地址的計算方法:
  1. IP地址與子網掩碼進行「與」運算,獲得網絡地址;
  2. 子網掩碼「取反」運算,而後與網絡地址進行「或」運算,獲得廣播地址;

如:172.17.24.18/20 ,計算其廣播地址;
  因爲該IP的掩碼爲20個比特位,所以,其掩碼地址爲:255.255.240.0
  IP地址的二進制表示爲:10101100.00010001.00011000.00010010
  (1)IP地址與子網掩碼按位「與」運算 結果:10101100.00010001.00010000.00000000 即:172.17.16.0
  (2)子網掩碼按位取反結果:00000000.00000000.00001111.11111111
  與網絡地址或運算結果:10101100.00010001.00011111.11111111 即:172.17.31.255

IP地址構成,由4個字節二進制數據表示,一般轉化成十進制形式:

上面看到了受限廣播地址,即 255.255.255.255 ,當使用這個地址做爲廣播地址時,路由器的其餘設備都能監聽到,但若是A路由器和B路由器想要之間也能通訊,好比:
A:ip爲192.168.134.7 ,子網掩碼爲 255.255.255.192
B:ip爲192.168.134.100 ,子網掩碼也是 255.255.255.192
看到A與B的子網掩碼是同樣,但其實仍是不能通訊,由於A與B的廣播地址不同,A廣播地址爲 192.168.134.63 B廣播地址爲 192.168.134.127。

5.2 端口號

網絡通訊,本質上是兩個進程(應用程序)的通訊。每臺計算機都有不少的進程,那麼在網絡通訊時,如何區分這些進程呢?
若是說IP地址能夠惟一標識網絡中的設備,那麼端口號就能夠惟一惟一標識設備中的進程(應用程序)了。

端口號是用兩個字節表示整數,它的取值範圍是0~65535。其中,0~1024之間的端口號用於一些知名的網絡服務和應用,普通的應用程序須要使用1024以上的端口號,若是端口號被另外一個服務或者程序所佔用,會致使當前程序啓動失敗。

利用協議+IP地址+端口號組合,就能夠標識網絡中的進程了,那麼進程間的通訊能夠利用這個標識與其它進程進行交互。

6. 案例實操,局域網搜索案例

實現一個局域網搜索案例:
首先綁定到本地主機上的30000端口,當接收到數據包時,拆數據包解析出要發送的端口號,而後隨機生產一個序列號,使用解析的端口號發送該序列號。

public class UdpProvider {
    public static void main(String[] args) throws IOException {

        // 建立DatagramSocket對象,並將其綁定到本地主機上的指定30000端口
        DatagramSocket socket = new DatagramSocket(30000);

        // 建立DatagramPacket對象,用於封裝接收的數據包
        byte[] receiveData = new byte[1024];
        DatagramPacket receivePacket = new DatagramPacket(receiveData, receiveData.length);

        // 接收數據包
        // 該方法阻塞,直到接收到數據報
        socket.receive(receivePacket);

        // 拆數據包
        int length = receivePacket.getLength();
        String ip = receivePacket.getAddress().getHostAddress();
        int port = receivePacket.getPort();
        String receivePort = new String(receiveData, 0, length);

        System.out.println("Provider-> receive: " + receivePort + " form " + ip + ":" + port);

        // 隨機生產一個序列號
        String sn = UUID.randomUUID().toString();
        // 根據接收到端口號,發送數據包
        byte[] data = sn.getBytes();
        DatagramPacket packet = new DatagramPacket(data, data.length);
        packet.setAddress(receivePacket.getAddress()); // 傳入接收過來的IP地址
        packet.setPort(Integer.parseInt(receivePort));

        // 使用DatagramSocket發送數據包
        socket.send(packet);

        // 釋放資源
        socket.close();
    }

}

綁定到本地主機上的20000端口,啓動後,給30000端口發送一個數據包,數據封裝20000端口數據。

public class UdpSearch {
    public static void main(String[] args) throws IOException, InterruptedException {

        CountDownLatch latch = new CountDownLatch(1);
        new Thread(new SearchListener(latch)).start();
        latch.await();

        sendBroadcast();
    }

    private static void sendBroadcast() throws IOException {

        // 建立DatagramSocket對象
        DatagramSocket socket = new DatagramSocket();

        // 建立DatagramPacket對象,用於封裝數據包:數據、IP地址、端口號
        byte[] data = "20000".getBytes();
        DatagramPacket packet = new DatagramPacket(data, data.length);
        packet.setPort(30000); // 數據包發給30000端口
        packet.setAddress(InetAddress.getByName("255.255.255.255"));

        //使用DatagramSocket的send方法,發送數據包
        socket.send(packet);

        socket.close();

        System.out.println("Search-> 發送廣播結束.");
    }

    private static class SearchListener implements Runnable {
        DatagramSocket socket;
        CountDownLatch latch;
        boolean isClosed = false;
        SearchListener(CountDownLatch latch) {
            this.latch = latch;
        }
        @Override
        public void run() {
            System.out.println("Search-> 已啓動...");
            latch.countDown();
            try {
                // 建立DatagramSocket對象,並將其綁定到本地主機上的指定端口。
                socket = new DatagramSocket(20000);

                while (!isClosed) {
                    byte[] receiverData = new byte[1024];
                    // 建立DatagramPacket對象,用於接收數據包
                    DatagramPacket receiverPacket = new DatagramPacket(receiverData, receiverData.length);

                    // 使用DatagramSocket對象,接收數據報包。
                    // 該方法阻塞,直到接收到數據報
                    socket.receive(receiverPacket);

                    // 拆接收的數據包
                    // 獲取接收數據報的IP地址
                    String ip = receiverPacket.getAddress().getHostAddress();
                    // 獲取數據報中的遠程主機上的端口號
                    int port = receiverPacket.getPort();
                    // 獲取接收到的數據的長度。
                    int length = receiverPacket.getLength();

                    // 數據緩衝區
                    byte[] buffer = receiverPacket.getData();

                    String data = new String(buffer, 0, length);
                    System.out.println("Search-> " + new Device(ip, port, data));
                }

            } catch (IOException ignore) {
            } finally {
                close();
            }
        }

        private void close() {
            if (socket != null) {
                //關閉數據報套接字。
                //全部當前阻塞的線程在receive(java.net.DatagramPacket)在此套接字將拋出一個SocketException 。
                socket.close();
            }
            socket = null;
        }

        private void exit() {
            isClosed = true;
            close();
        }
    }
}

若是個人文章對您有幫助,不妨點個贊鼓勵一下(^_^)

相關文章
相關標籤/搜索