UDP協議之快速入門

目錄

前言

一、什麼是UDP協議

1、UDP的概念

2、爲什麼說UDP是不可靠的?

3、UDP的實際業務場景(就是它能做什麼)

4、UDP報文頭解析

5、UDP包最大長度

二、UDP核心API

1、API-DatagramSocket

2、API-DatagramPacket

三、UDP單播、廣播、多播

1、單播、多播、廣播的概念

2、IP地址類別

3、廣播地址

4、IP地址的構成

5、廣播地址運算

6、廣播通信問題

四、實戰案例

1、局域網搜索案例

2、局域網廣播搜索


前言

上一篇我們已經說了關於TCP的一些知識,本篇是該系列的第二篇,帶你認識UDP協議,我會首先分享一些關於UDP的基礎知識,然後再來做一個小案例,加深一下對UDP的理解。

一、什麼是UDP協議

1、UDP的概念

UDP的英文:User Datagram Protocol,縮寫爲UDP。它是一種用戶數據報協議,又稱用戶數據報文協議,是一個簡單的面向數據報傳輸層協議,正式規範爲RFC 768,是用戶數據協議,非連接協議,這一點與TCP不同。

2、爲什麼說UDP是不可靠的?

我們平常應該都聽說過UDP是不可靠的這句話,但是它究竟爲甚不可靠呢,這句話又是個什麼意思呢?下面就來說一下爲什麼:

因爲UDP它本身並不是像TCP那樣是面向連接的,它是非連接協議,它是客戶端發送協議,服務器端從網絡中抓取協議,抓取的時間以及發送的時間如果不同,可能會導致客戶端發送的數據服務器端沒有接收到,這個想必大家在大學學習計算機網絡的時候都瞭解過,簡單來說就是這玩意容易丟包,所以給大家造成了不靠譜的印象,在UDP中其實是沒有標準的客戶端與服務器端的。

總結起來就是以下幾點:

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

3、UDP的實際業務場景(就是它能做什麼)

  • DNS(域名和IP相互轉換的一種協議)、TFTP(一種文件傳輸的協議)、SNMP(網絡數據傳輸中的一個監控的協議)
  • 視頻、音頻、普通數據(無關緊要的數據)

4、UDP報文頭解析

這張圖是告訴我們UDP的報文頭主要是做了什麼事情,也就是UDP協議的前面加了哪些東西,可以看到它一共有64位,前16位是發送源端口,後面16位是目標端口,接着又是16位是字節長度,最後的16位是頭部和數據校驗字段。

5、UDP包最大長度

  • 16位——>2字節 存儲長度信息
  • 2^16-1 = 64K-1 = 65536-1=65535
  • 自身協議佔用:32+32 = 64位=8字節
  • 65535-8=65507byte

二、UDP核心API

1、API-DatagramSocket

  • 用於接收與發送UDP的類
  • 負責發送某一個UDP包,或者接收UDP包
  • 不同於TCP,UDP並沒有合併到Socket API中
  • DatagramSocket() 構造函數,創建簡單實例,不指定端口與IP
  • DatagramSocket(int port) 創建監聽固定端口的實例
  • DatagramSocket(int port,InetAddress localAddr) 創建固定端口指定IP的實例
  • receive(DatagramPacket d):接收
  • send(DatagramPacket d):發送
  • setSoTimeout(int timeout):設置超時,毫秒
  • close():關閉、釋放資源

2、API-DatagramPacket

  • 用於處理報文
  • 將byte數組、目標地址、目標端口等數據包裝成報文或者將報文拆卸成byte數組
  • 是UDP的發送實體,也是接收實體
  • DatagramPacket(byte[] buf,int offset,int length,InetAddress address,int port) 前面三個參數指定buf的使用區間,後面兩個參數指定目標機器地址與端口
  • DatagramPacket(byte[] buf,int length,SocketAddress address) 前面兩個參數指定buf的使用區間,SocketAddress相當於InetAddress+port
  • setData(byte[] buf,int offset,int length)
  • setData(byte[] buf)
  • setLength(int length)
  • getData()、getOffset()、getLength()
  • setAddress(InetAddress iaddr)、setPort(int iport)
  • getAddress()、getPort()
  • setSocketAddress(SocketAddress address)
  • getSocketAddress()

三、UDP單播、廣播、多播

1、單播、多播、廣播的概念

單播:點對點,比如我的電腦和你的電腦之間發送數據,你的電腦也和我的電腦之間回送數據,這兩者之間的數據不被其他電腦所感知,這個就稱之爲單播,用於單線兩者之間與其他人無關

多播:更準確的來說應該叫組播,是給具體的某一組發送數據,比如你在班級裏說所有的男生站起來,你是給你們班的男生髮送數據,這部分男生可以成爲一個組,跟其他女生無關

廣播:給所有的設備都發送信息,你在廣場上喊了一句,所有人都能收到你的信息,至於究竟哪些人對這個信息感興趣,這個就是無關的了,可能會有一部分人認爲這個信息與我無關我不處理,這就是廣播的概念,主要是用於你對你所在的網段的所有設備發送信息,這也會產生一個問題,如果某一個設備或者某一批設備一直髮送廣播的話,會導致局域網或者某個網段內的帶寬被佔滿,也就導致了信息的混亂,所以現在的路由器都具備拒絕發送廣播的策略,一般只能在你路由器內部發送廣播。

2、IP地址類別

3、廣播地址

  • 255.255.255.255 爲受限廣播地址
  • C網廣播地址一般爲:XXX.XXX.XXX.255(192.168.1.255)
  • D類IP地址爲多播預留

4、IP地址的構成

5、廣播地址運算

這裏舉個栗子說明一下吧:

  • IP:192.168.124.7
  • 子網掩碼:255.255.255.192
  • 網絡地址:192.168.124.0
  • 廣播地址:192.168.124.63
  • 255.255.255.192->11111111.11111111.11111111.11000000
  • 可劃分網段:2^2 = 4 個
  • 即:0~63、64~127、128~191、192~255
  • 又由於我的IP是192.168.124.7,處在第一個網段,則廣播地址取該網段最後一個地址即:192.168.124.63

6、廣播通信問題

這裏也是舉個栗子說明一下:

主機一:192.168.124.7,子網掩碼:255.255.255.192

主機二:192.168.124.100,子網掩碼:255.255.255.192

這兩者之間可以通信嗎?。。。。。。。。。。。。。。。。。。。。。。。。。。。。。。。。。當然不能

主機一廣播地址:192.168.124.63

主機二廣播地址:192.168.124.127 兩者不在一個網段內哦,所以無法通信!

四、實戰案例

1、局域網搜索案例

import java.io.IOException;
import java.net.DatagramPacket;
import java.net.DatagramSocket;

/**
 * UDP提供者,用於提供服務
 */
public class UDPProvider {
    public static void main(String[] args) throws IOException {
        System.out.println("UDPProvider---------開始.");
        //作爲接收者,指定一個端口用於數據接收
        DatagramSocket ds = new DatagramSocket(20000);

        //構建接收實體
        final byte[] buf = new byte[512];
        DatagramPacket receivePack = new DatagramPacket(buf, buf.length);

        //接收
        ds.receive(receivePack);

        //打印接收到的信息與發送者的信息
        //發送者的IP地址
        String ip = receivePack.getAddress().getHostAddress();
        int port = receivePack.getPort();
        int dataLen = receivePack.getLength();
        String data = new String(receivePack.getData(), 0, dataLen);
        System.out.println("UDPProvider receive from ip:" + ip
                + "\tport:" + port + "\tdata:" + data);

        //構建一份回送數據
        String responseData = "Receive data with len:" + dataLen;
        byte[] responseDataBytes = responseData.getBytes();
        DatagramPacket responsePack = new DatagramPacket(responseDataBytes,
                responseDataBytes.length, receivePack.getAddress(), receivePack.getPort());
        ds.send(responsePack);

        //完成
        System.out.println("UDPProvider---------完成.");
        ds.close();
    }
}
import java.io.IOException;
import java.net.DatagramPacket;
import java.net.DatagramSocket;
import java.net.InetAddress;

/**
 * UDP搜索者,用於搜索服務支持方
 */
public class UDPSearcher {
    public static void main(String[] args) throws IOException {
        System.out.println("UDPSearcher---------開始.");
        //作爲搜索方,無需指定端口,讓系統自動分配
        DatagramSocket ds = new DatagramSocket();

        //構建一份請求數據
        String requestData = "Hello Socket!";
        byte[] requestDataBytes = requestData.getBytes();
        //直接構建packet
        DatagramPacket requestPack = new DatagramPacket(requestDataBytes,
                requestDataBytes.length);
        requestPack.setAddress(InetAddress.getLocalHost());
        requestPack.setPort(20000);

        //發送
        ds.send(requestPack);


        //構建接收實體
        final byte[] buf = new byte[512];
        DatagramPacket receivePack = new DatagramPacket(buf, buf.length);

        //接收
        ds.receive(receivePack);

        //打印接收到的信息與發送者的信息
        //發送者的IP地址
        String ip = receivePack.getAddress().getHostAddress();
        int port = receivePack.getPort();
        int dataLen = receivePack.getLength();
        String data = new String(receivePack.getData(), 0, dataLen);
        System.out.println("UDPSearcher receive from ip:" + ip
                + "\tport:" + port + "\tdata:" + data);

        //完成
        System.out.println("UDPSearcher---------完成.");
        ds.close();
    }
}

上面兩個類分別定義了服務接收方和搜索方,代碼裏面也都有註釋,我就不多講了,下面來看下結果:

2、局域網廣播搜索

這個案例我們是在上個搜索的基礎上來實現,所以是對代碼做一些修改。

首先我們新建一個用於構建消息的類,代碼如下:

/**
 * 消息構建者
 */
public class MessageCreator {
    private static final String SN_HEADER = "收到口令,我是(SN):";
    private static final String PORT_HEADER = "這是口令,請回送端口(Port)";

    public static String buildWithPort(int port) {
        return PORT_HEADER + port;
    }

    public static int parsePort(String data) {
        if (data.startsWith(PORT_HEADER)) {
            return Integer.parseInt(data.substring(PORT_HEADER.length()));
        }
        return -1;
    }

    public static String buildWithSn(String sn) {
        return SN_HEADER + sn;
    }

    public static String parseSn(String data) {
        if (data.startsWith(SN_HEADER)) {
            return data.substring(SN_HEADER.length());
        }
        return null;
    }
}

然後修改UDPProvider:

import java.io.IOException;
import java.net.DatagramPacket;
import java.net.DatagramSocket;
import java.util.UUID;

/**
 * UDP提供者,用於提供服務
 */
public class UDPProvider {

    public static void main(String[] args) throws IOException {
        //生成一份唯一標識
        String sn = UUID.randomUUID().toString();
        Provider provider = new Provider(sn);
        provider.start();

        //讀取任意鍵盤信息後可以退出
        System.in.read();
        provider.exit();
    }

    //創建線程類
    private static class Provider extends Thread {
        private final String sn;
        private boolean done = false;
        private DatagramSocket ds = null;

        public Provider(String sn) {
            super();
            this.sn = sn;
        }

        @Override
        public void run() {
            super.run();
            System.out.println("UDPProvider---------開始.");

            try {
                //監聽20000端口
                ds = new DatagramSocket(20000);

                while (!done) {

                    //構建接收實體
                    final byte[] buf = new byte[512];
                    DatagramPacket receivePack = new DatagramPacket(buf, buf.length);

                    //接收
                    ds.receive(receivePack);

                    //打印接收到的信息與發送者的信息
                    //發送者的IP地址
                    String ip = receivePack.getAddress().getHostAddress();
                    int port = receivePack.getPort();
                    int dataLen = receivePack.getLength();
                    String data = new String(receivePack.getData(), 0, dataLen);
                    System.out.println("UDPProvider receive from ip:" + ip
                            + "\tport:" + port + "\tdata:" + data);

                    //解析端口號
                    int responsePort = MessageCreator.parsePort(data);

                    if (responsePort != -1) {
                        //構建一份回送數據
                        String responseData = MessageCreator.buildWithSn(sn);
                        byte[] responseDataBytes = responseData.getBytes();
                        DatagramPacket responsePack = new DatagramPacket(responseDataBytes,
                                responseDataBytes.length, receivePack.getAddress(),
                                responsePort);
                        ds.send(responsePack);
                    }
                }
            } catch (Exception ignored) {
            } finally {
                close();
            }
            //完成
            System.out.println("UDPProvider---------完成.");
        }

        /**
         * 提供結束
         */
        private void close() {
            if (ds != null) {
                ds.close();
                ds = null;
            }
        }

        void exit() {
            done = true;
            close();
        }
    }
}

最後還需要修改UDPSearcher:

import java.io.IOException;
import java.net.DatagramPacket;
import java.net.DatagramSocket;
import java.net.InetAddress;
import java.util.ArrayList;
import java.util.List;
import java.util.concurrent.CountDownLatch;

/**
 * UDP搜索者,用於搜索服務支持方
 */
public class UDPSearcher {
    private static final int LISTEN_PORT = 30000;

    public static void main(String[] args) throws IOException, InterruptedException {
        System.out.println("UDPSearcher---------開始.");
        Listener listener = listen();
        sendBroadcast();

        //讀取任意鍵盤信息後可以退出
        System.in.read();

        List<Device> devices = listener.getDevicesAndClose();
        for (Device device:devices){
            System.out.println("Device:"+device.toString());
        }

        //完成
        System.out.println("UDPSearcher---------完成.");
    }

    private static Listener listen() throws InterruptedException {
        System.out.println("UDPSearcher start listener.");
        CountDownLatch countDownLatch = new CountDownLatch(1);
        Listener listener = new Listener(LISTEN_PORT,countDownLatch);
        listener.start();
        countDownLatch.await();
        return listener;
    }

    private static void sendBroadcast() throws IOException {
        System.out.println("UDPSearcher sendBroadcast---------開始.");
        //作爲搜索方,無需指定端口,讓系統自動分配
        DatagramSocket ds = new DatagramSocket();

        //構建一份請求數據
        String requestData = MessageCreator.buildWithPort(LISTEN_PORT);
        byte[] requestDataBytes = requestData.getBytes();
        //直接構建packet
        DatagramPacket requestPack = new DatagramPacket(requestDataBytes,
                requestDataBytes.length);
        //20000端口,廣播地址
        requestPack.setAddress(InetAddress.getByName("255.255.255.255"));
        requestPack.setPort(20000);

        //發送
        ds.send(requestPack);
        ds.close();

        //完成
        System.out.println("UDPSearcher sendBroadcast---------完成.");
    }

    private static class Device {
        final int port;
        final String ip;
        final String sn;

        public Device(int port, String ip, String sn) {
            this.port = port;
            this.ip = ip;
            this.sn = sn;
        }

        @Override
        public String toString() {
            return "Device{" +
                    "port=" + port +
                    ", ip='" + ip + '\'' +
                    ", sn='" + sn + '\'' +
                    '}';
        }
    }

    private static class Listener extends Thread {
        private final int listenPort;
        private final CountDownLatch countDownLatch;
        private final List<Device> devices = new ArrayList<>();
        private boolean done = false;
        private DatagramSocket ds = null;

        public Listener(int listenPort, CountDownLatch countDownLatch) {
            this.listenPort = listenPort;
            this.countDownLatch = countDownLatch;
        }

        @Override
        public void run() {
            super.run();
            //通知已啓動
            countDownLatch.countDown();
            try {
                //監聽回送端口
                ds = new DatagramSocket(listenPort);
                while (!done) {
                    //構建接收實體
                    final byte[] buf = new byte[512];
                    DatagramPacket receivePack = new DatagramPacket(buf, buf.length);

                    //接收
                    ds.receive(receivePack);

                    //打印接收到的信息與發送者的信息
                    //發送者的IP地址
                    String ip = receivePack.getAddress().getHostAddress();
                    int port = receivePack.getPort();
                    int dataLen = receivePack.getLength();
                    String data = new String(receivePack.getData(), 0, dataLen);
                    System.out.println("UDPSearcher receive from ip:" + ip
                            + "\tport:" + port + "\tdata:" + data);

                    String sn = MessageCreator.parseSn(data);
                    if (sn!=null){
                        Device device = new Device(port,ip,sn);
                        devices.add(device);
                    }
                }
            } catch (Exception ignored) {
            } finally {
                close();
            }
            System.out.println("UDPSearcher listener finished");
        }

        private void close() {
            if (ds != null) {
                ds.close();
                ds = null;
            }
        }

        List<Device> getDevicesAndClose() {
            done = true;
            close();
            return devices;
        }

    }
}

來看一下運行結果,對於UDPProvider來說它其實是無限循環一直存在的,我們可以多次發送,如下圖所示:

發表評論
所有評論
還沒有人評論,想成為第一個評論的人麼? 請在上方評論欄輸入並且點擊發布.
相關文章