1. 程式人生 > >Android收發UDP報文詳解 及 優雅解決接收不到問題

Android收發UDP報文詳解 及 優雅解決接收不到問題

前段時間專案組接了一個研究所專案,移動端這邊需要做一個UDP接收報文的程式APP,其中還涉及到多頁面之間收發報文、動態修改地址、埠號等等。原本編寫這個收發程式並不難,步驟也比較固定,在網上找了相關例子進行二次開發,可是發現UDP報文接收不到,這其中還是隱藏著某些坑,僅以此篇文章來總結其奧妙精髓。

一. DatagramSocket收發報文相關知識點

此節將講解資料報通訊的原理,即不需要面向連線的通訊方式,資料報通訊方式採用的是UDP(User Datagram Protocol)協議,這裡同TCP作比較再次介紹。(對相關知識不熟悉者,可先看此篇部落格稍作了解: http://blog.csdn.net/itermeng/article/details/72970099

1. UDP與TCP學習

  • UDP(User Datagram Protocol)
    非面向連線的提供不可靠的資料包式的資料傳輸協議。類似於從郵局傳送信件的過程,傳送信件是通過郵局系統一站一站進行傳遞,中間也有可能丟失。Java中有些類是基於UDP協議來進行網路通訊的,有DatagramPacket、DatagramSocket、MulticastSocket等類。

  • TCP(Transport Control Protocol)
    面向連線的能夠提供可靠的流式資料傳輸的協議。類似於打電話的過程,在撥完電話後兩者之間會先建立連線,為了更好的通話,確保通話連線後兩者開始互相傳輸資訊。相對應的類有URL、URLConnection Socket、ServerSocket等

    UDP 與 TCP的區別

  • TCP有建立時間,UDP無

  • UDP傳輸有大小限制,每一個數據報需在64K以內
  • TCP常見應用:Telnet遠端登入、Ftp檔案傳輸
  • UDP常見應用:ping指令,用來檢測網路上某臺伺服器是否還在提供服務。

2. DatagramSocket學習

(1)定義概念

此類表示用來發送和接收資料報包的套接字。

資料報套接字是包投遞服務的傳送或接收點。每個在資料報套接字上傳送或接收的包都是單獨編址和路由的。從一臺機器傳送到另一臺機器的多個包可能選擇不同的路由,也可能按不同的順序到達。

在 DatagramSocket 上總是啟用 UDP 廣播發送。為了接收廣播包,應該將 DatagramSocket 繫結到萬用字元地址。在某些實現中,將 DatagramSocket 繫結到一個更加具體的地址時廣播包也可以被接收。

(2)建構函式總結

建構函式名稱 含義
DatagramSocket() 構造資料報套接字並將其繫結到本地主機上任何可用的埠。
DatagramSocket(int port) 建立資料報套接字並將其繫結到本地主機上的指定埠。
DatagramSocket(int port, InetAddress laddr) 建立資料報套接字,將其繫結到指定的本地地址。

(3)重要方法摘要

方法名稱 含義
void close() 關閉此資料報套接字。
void connect(InetAddress address, int port) 將套接字連線到此套接字的遠端地址。
boolean isClosed() 返回是否關閉了套接字。
void receive(DatagramPacket p) 從此套接字接收資料報包。
void send(DatagramPacket p) 從此套接字傳送資料報包。
DatagramSocket(int port, InetAddress laddr) 建立資料報套接字,將其繫結到指定的本地地址。

3. InetAddress學習

(1)定義概念

此類表示網際網路協議 (IP) 地址。

IP 地址是 IP 使用的 32 位或 128 位無符號數字,它是一種低階協議,UDP 和 TCP 協議都是在它的基礎上構建的。InetAddress 的例項包含 IP 地址,還可能包含相應的主機名(取決於它是否用主機名構造或者是否已執行反向主機名解析)。

(2)建立方法
注意,建立此類事通過類方法而獲取,並非構造方法。

方法名稱 含義
static InetAddress getByAddress(byte[] addr) 在給定原始 IP 地址的情況下,返回 InetAddress 物件。
static InetAddress getByAddress(String host, byte[] addr) 根據提供的主機名和 IP 地址建立 InetAddress。
static InetAddress getByName(String host) 在給定主機名的情況下確定主機的 IP 地址。

4. DatagramPacket學習

(1)定義概念

此類表示資料報包。

資料報包用來實現無連線包投遞服務。每條報文僅根據該包中包含的資訊從一臺機器路由到另一臺機器。從一臺機器傳送到另一臺機器的多個包可能選擇不同的路由,也可能按不同的順序到達。不對包投遞做出保證。

(2)構造方法

建構函式名稱 含義
DatagramPacket(byte[] buf, int length) 用來接收長度為 length 的資料包
DatagramPacket(byte[] buf, int length, InetAddress address, int port) 構造資料報包,用來將長度為 length 的包傳送到指定主機上的指定埠號

二. 傳送、接收UDP報文步驟講解

在此程式中,將UDP報文的傳送、接收操作分別寫入到兩個執行緒中,根據具體需求進行呼叫。

1. 資料報傳送 解析

以下步驟都在傳送執行緒內,需要注意的是傳送UDP報文邏輯單一,順序執行完畢執行緒即可結束,不涉及到後臺等待的需求,所以執行完後即可關閉套接字連線

傳送步驟

  • 構造DatagramSocket物件
  • 根據傳送IP 來建立InetAddress物件
  • 根據InetAddress物件、傳送埠號、傳送資料 來建立傳送的DatagramPacket資料包物件
  • 呼叫DatagramSocket物件的send(datagramPacket) 方法,傳送UDP報文
  • 呼叫DatagramSocket物件的close() 關閉套接字連線

對應以上步驟,程式碼展示(僅為部分重要程式碼):

    Byte[] buf="hello android! ".getBytes();
    DatagramSocket sendSocket = new DatagramSocket();
    InetAddress serverAddr = InetAddress.getByName(SEND_IP);
    DatagramPacket outPacket = new DatagramPacket(buf, buf.length,serverAddr, SEND_PORT);
    sendSocket.send(outPacket);
    sendSocket.close();

2. 資料報接收 解析

原理
以下步驟都在接收執行緒內,同傳送執行緒大致相同,但需要注意這兩者本質的區別:傳送執行緒裡的邏輯執行一遍即可結束,但是接收執行緒需要在後臺待定等待接收UDP報文,不可執行一遍就結束!相當於在一個介面中,可多次建立傳送執行緒用來發送報文,但是接收執行緒只需在介面初始化時建立,從而一直監聽報文接收(若重新進入介面,邏輯如上)。

所以,在接收執行緒內部需要用到迴圈,在迴圈內部呼叫套接字物件的receive() 方法來接收UDP報文。注意套接字物件的連線關閉,傳送執行緒中單一的邏輯,執行完傳送過程即可關閉連線,但是在接收執行緒中使用了迴圈,所以需要用一個全域性標識量來控制迴圈,若介面退出或銷燬則將標示值置為false,這樣接收執行緒即可結束,再關閉套接字連線。

接收步驟

  • 需要根據接收埠號 構造DatagramSocket物件
  • 建立傳送的DatagramPacket資料包物件
  • 呼叫DatagramSocket物件的receive(datagramPacket) 方法,接收UDP報文

對應以上步驟,程式碼展示(僅為部分重要程式碼):

    DatagramSocket receiveSocket = new DatagramSocket(RECEIVE_PORT);

    while(listenStatus){
         byte[] inBuf= new byte[1024];
         DatagramPacket inPacket=new DatagramPacket(inBuf,inBuf.length);
         receiveSocket.receive(inPacket);                                           i        if(!inPacket.getAddress().equals(serverAddr)){
             throw new IOException("未知名的報文");
          }

         receiveInfo = inPacket.getData();
         receiveHandler.sendEmptyMessage(1);
     }

三. 介面收發UDP報文完整程式碼

public class TextActivity extends AppCompatActivity{
    /*
    *   Data
    * */
    private final static String SEND_IP = "27.18.140.100";  //傳送IP
    private final static int SEND_PORT = 8989;               //傳送埠號
    private final static int RECEIVE_PORT = 8080;            //接收埠號

    private boolean listenStatus = true;  //接收執行緒的迴圈標識
    private byte[] receiveInfo;     //接收報文資訊
    private byte[] buf;

    private DatagramSocket receiveSocket;
    private DatagramSocket sendSocket;
    private InetAddress serverAddr;
    private SendHandler sendHandler = new SendHandler();
    private ReceiveHandler receiveHandler = new ReceiveHandler();

    /*
    *   UI
    * */
    private TextView tvMessage;
    private Button btnSendUDP;

    class ReceiveHandler extends Handler{
        @Override
        public void handleMessage(Message msg) {
            super.handleMessage(msg);
            tvMessage.setText("接收到資料了" + receiveInfo.toString());
            Toast.makeText(TextActivity.this, "接收到資料了", Toast.LENGTH_SHORT).show();
        }
    }

    class SendHandler extends Handler{
        @Override
        public void handleMessage(Message msg) {
            super.handleMessage(msg);
            tvMessage.setText("UDP報文傳送成功");
            Toast.makeText(TextActivity.this, "成功傳送", Toast.LENGTH_SHORT).show();
        }
    }

    @Override
    protected void onCreate(Bundle savedInstanceState) {
        super.onCreate(savedInstanceState);
        setContentView(R.layout.activity_text);

        btnSendUDP = (Button) findViewById(R.id.btn_send);
        tvMessage = (TextView) findViewById(R.id.tv_show);

        btnSendUDP.setOnClickListener(new View.OnClickListener() {
            @Override
            public void onClick(View view) {
                //點選按鈕則傳送UDP報文
                new UdpSendThread().start();
            }
        });

        //進入Activity時開啟接收報文執行緒
        new UdpReceiveThread().start();
    }

    @Override
    protected void onDestroy() {
        super.onDestroy();
        //停止接收執行緒,關閉套接字連線
        listenStatus = false;
        receiveSocket.close();
    }

    /*
     *   UDP資料傳送執行緒
     * */
    public class UdpSendThread extends Thread
    {
        @Override
        public void run()
        {
            try
            {
                buf="i am an android developer, hello android! ".getBytes();

                // 建立DatagramSocket物件,使用隨機埠
                sendSocket = new DatagramSocket();
                serverAddr = InetAddress.getByName(SEND_IP);

                DatagramPacket outPacket = new DatagramPacket(buf, buf.length,serverAddr, SEND_PORT);
                sendSocket.send(outPacket);

                sendSocket.close();
                sendHandler.sendEmptyMessage(1);

            } catch (Exception e)
            {
                e.printStackTrace();
            }
        }
    }

    /*
    *   UDP資料接收執行緒
    * */
    public class UdpReceiveThread extends Thread
    {
        @Override
        public void run()
        {
            try
            {

                sendSocket = new DatagramSocket(RECEIVE_PORT);
                serverAddr = InetAddress.getByName(SEND_IP);

                while(listenStatus)
                {
                    byte[] inBuf= new byte[1024];
                    DatagramPacket inPacket=new DatagramPacket(inBuf,inBuf.length);
                    sendSocket.receive(inPacket);

                    if(!inPacket.getAddress().equals(serverAddr)){
                        throw new IOException("未知名的報文");
                    }

                    receiveInfo = inPacket.getData();
                    receiveHandler.sendEmptyMessage(1);
                }
            } catch (Exception e)
            {
                e.printStackTrace();
            }
        }
    }


}

四. 優雅解決UDP報文接收不到問題

以上程式碼已經可以實現傳送、接收UDP報文了,至少我自測是沒有問題的,是修改好的版本。一開始我實在網上找的版本進行二次修改的,只是那程式碼有些小缺陷,導致有時候接收UDP報文有問題,前2個原因是根據此次工作發現並解決了此問題,如果前兩點未解決,第三點是另一種嘗試。

1. 傳送與接收DatagramSocket需不同

此點很重要,之前在網上找的例子就將接收、傳送執行緒中的DatagramSocket共用同一個全域性變數,這會直接導致應用程式無法接收到UDP報文!

首先接收、傳送UDP的邏輯分別在兩個不同的執行緒中的run() 方法裡,根據執行緒的啟動去呼叫它們。注意兩個執行緒存在的生命週期

  • 接收執行緒 在一個介面中(不退出介面的情況下)只會被建立並啟動一次,即執行緒一直存在於後臺等待接收UDP報文,此時它的DatagramSocket物件也是要一直存在的;

  • 傳送執行緒 在需要的情況下會多次被重複建立並啟動,它的每次啟動都會去建立DatagramSocket物件,傳送完報文後會立即關閉掉DatagramSocket的連線。

若兩個執行緒共用一個DatagramSocket物件,接收執行緒開啟後,DatagramSocket物件存在於後臺,此時傳送一次報文後,DatagramSocket物件會被關閉連線,這樣應用程式就無法再接收到UDP報文了。所以,這兩個執行緒的DatagramSocket物件需獨立不相同!

2. 退出介面需關閉接收Socket連線

在理解了上一點後,在不退出介面的情況下應用程式可以很好的收發UDP報文,再思考全面一點。考慮使用者在使用中退出介面又重新回到介面,重點還是放在DatagramSocket物件上,一般出錯大多是出在這個上面。

  • 傳送執行緒執行完邏輯後會關閉DatagramSocket連線,執行緒結束。

  • 可是接收執行緒是一直執行在後臺,除非迴圈標識量置為False,接收執行緒才會結束。

如果不對接收執行緒中的DatagramSocket物件進行處理,在退出介面又重新回到介面時,接收執行緒會被重新建立,之前建立的DatagramSocket物件連線未關閉,此時再重新建立,便會出現異常,導致應用程式無法接收到UDP報文。異常如下:

java.net.BindException: bind failed: EADDRINUSE (Address already in use)

很明顯,顯示地址被佔用。所以介面退出時應對接收執行緒的回收及Socket物件的連線關閉,修改程式碼如下:

    @Override
    protected void onDestroy() {
        super.onDestroy();
        //停止接收執行緒,關閉套接字連線
        listenStatus = false;
        receiveSocket.close();
    }

3. Rom關閉了手機接收UDP報功能

我的程式在修改完以上後基本沒有出什麼問題了,但是這點原因在網上出現的概率很大,遂一起寫進來,為讀者拓展思維。有的手機不能直接接收UDP包,可能是手機廠商在定製Rom的時候把這個功能給關掉了,解決辦法如下:

(1)可先在oncreate()方法裡面例項化一個WifiManager.MulticastLock 物件lock;具體如下:

WifiManager manager = (WifiManager) this
                .getSystemService(Context.WIFI_SERVICE);
WifiManager.MulticastLock lock= manager.createMulticastLock("test wifi");

(2)在呼叫廣播發送、接收報文之前先呼叫lock.acquire()方法;
(3)用完之後及時呼叫lock.release()釋放資源,否決多次呼叫lock.acquire()方法,程式可能會崩,詳情請見

Caused by: java.lang.UnsupportedOperationException: Exceeded maximum number of wifi locks

注:記得在配置檔案裡面新增如下許可權:

<uses-permission android:name="android.permission.CHANGE_WIFI_MULTICAST_STATE" />

經過這樣處理後,多數手機都能正常傳送接收到廣播報文。

五. 測試環境建議

關於這個測試環境,我是藉助手機和電腦來完成的,可能大多數人用的是 Wireshark 軟體來測試UDP報文收發,但是對於新手而言需要學習一會,這裡推薦一個上手難度為0的測試軟體:網路除錯助手NetAsssist,資源在最後。

用法
將手機和電腦連線在同一區域網下,我這裡是將兩者全部連線校園Wifi,然後開啟電腦軟體,上面自動顯示電腦分配的IP,檢視手機連線Wifi高階設定獲取手機分配IP。選擇好網路除錯助手軟體的協議型別和埠號,手機可傳送報文到電腦上,在電腦網路除錯助手軟體上填寫好“手機IP:接收埠號”,點擊發送按鈕,手機即可接收到電腦傳送的UDP報文。

軟體使用截圖:

這裡寫圖片描述

程式應用測試截圖:

這裡寫圖片描述 這裡寫圖片描述

最後說明一下,關於所做的專案需求略微複雜,但是應用程式的骨幹精華就是第三小節的那些程式碼,而一些坑及需要注意的問題在第四點提及,這次接觸UDP報文,著實是邊學邊摸索,總算從坑裡爬了出來,可能我所記錄的並不詳細或者有誤,虛心請教~

希望對你們有幫助:)