在 Android 上,一個(gè)完整的 UDP 通信模塊應(yīng)該是怎樣的?

在 Android 上,一個(gè)完整的 UDP 通信模塊應(yīng)該是怎樣的?

本文原創(chuàng),轉(zhuǎn)載請(qǐng)注明出處。
歡迎關(guān)注我的 簡(jiǎn)書 ,關(guān)注我的專題 Android Class 我會(huì)長(zhǎng)期堅(jiān)持為大家收錄簡(jiǎn)書上高質(zhì)量的 Android 相關(guān)博文。

本文例子的所有代碼在這里:

github 連接在這里 UDPSocketDemo

寫在前面:
在上周寫了一篇關(guān)于 TCP 和 UDP 概念區(qū)別對(duì)比的文章:

TCP與UDP差異對(duì)比分析

在這篇文章中,在可靠性、數(shù)據(jù)發(fā)送、適用場(chǎng)景等多個(gè)方面分析了二者的區(qū)別。而本文的目的是想給大家介紹下在 Android 設(shè)備上,一個(gè)手機(jī)通過熱點(diǎn)連接另一個(gè)手機(jī)。這種場(chǎng)景下,完整的 UDP 通信模塊應(yīng)該考慮哪些方面,又應(yīng)該如何優(yōu)化,如何避開一些坑呢?

UDP 在 Java 中的使用

我們都知道,開發(fā)一個(gè) Android 應(yīng)用程序,目前大多數(shù)還是使用的是 Java 語言。在 Java 語言中怎么去使用 UDP 協(xié)議呢?

上篇文章中我們沒說 Socket,其實(shí) Socket 可以理解為對(duì) TCP、UDP 協(xié)議在程序使用層面的封裝,提供出一些 api 來供程序員調(diào)用開發(fā),這就是 Socket 最表層的含義。

在 Java 中,與 UDP 相關(guān)的類有 DatagramSocket、DatagramPacket 等,關(guān)于他們的使用,這里不著重介紹,可以看下面這篇文章:

Java UDP Socket

好了,假設(shè)大家對(duì)他們的使用都已大概了解,可以正式開始本文的內(nèi)容了。

初始化一個(gè) UDPSocket

首先創(chuàng)建一個(gè)叫 UDPSocket 的類。

    public UDPSocket(Context context) {

        this.mContext = context;

        int cpuNumbers = Runtime.getRuntime().availableProcessors();
        // 根據(jù)CPU數(shù)目初始化線程池
        mThreadPool = Executors.newFixedThreadPool(cpuNumbers * POOL_SIZE);
        // 記錄創(chuàng)建對(duì)象時(shí)的時(shí)間
        lastReceiveTime = System.currentTimeMillis();
    }

在構(gòu)造方法里,我們進(jìn)行下一些初始化操作,簡(jiǎn)單來說就是創(chuàng)建一個(gè)線程池,記錄一下當(dāng)前時(shí)間毫秒值,至于他們有什么用,再往下看:

    public void startUDPSocket() {
        if (client != null) return;
        try {
            // 表明這個(gè) Socket 在設(shè)置的端口上監(jiān)聽數(shù)據(jù)。
            client = new DatagramSocket(CLIENT_PORT);

            if (receivePacket == null) {
                // 創(chuàng)建接受數(shù)據(jù)的 packet
                receivePacket = new DatagramPacket(receiveByte, BUFFER_LENGTH);
            }

            startSocketThread();
        } catch (SocketException e) {
            e.printStackTrace();
        }
    }

這里我們首先創(chuàng)建了一個(gè) DatagramSocket 作為“客戶端”,其實(shí) UDP 本身沒有客戶端和服務(wù)端的概念,只有發(fā)送方和接收方的概念,我們把發(fā)送方暫時(shí)當(dāng)成是一個(gè)客戶端吧。

創(chuàng)建 DatagramSocket 對(duì)象時(shí),傳入了一個(gè)端口號(hào),這個(gè)端口號(hào)可以在一個(gè)范圍內(nèi)自己定義,表示這個(gè) DatagramSocket 在此端口上監(jiān)聽數(shù)據(jù)。

然后又創(chuàng)建了一個(gè) DatagramPacket 對(duì)象,作為數(shù)據(jù)的接收包。

最后調(diào)用 startSocketThread 啟動(dòng)發(fā)送和接收數(shù)據(jù)的線程。

    /**
     * 開啟發(fā)送數(shù)據(jù)的線程
     */
    private void startSocketThread() {
        clientThread = new Thread(new Runnable() {
            @Override
            public void run() {
                Log.d(TAG, "clientThread is running...");
                receiveMessage();
            }
        });
        isThreadRunning = true;
        clientThread.start();

        startHeartbeatTimer();
    }

首先 clientThread 線程的目的是調(diào)用 DatagramSocket receive 方法,因?yàn)?receive 方法是阻塞的,不能放在主線程,所以自然開啟一個(gè)子線程了。receiveMessage 就是處理接受到的 UDP 數(shù)據(jù)報(bào),我們先不看接受數(shù)據(jù)的這個(gè)方法,畢竟還沒人發(fā)消息呢,自然就談不上收了。

心跳包保持“長(zhǎng)連接”

來到本文的第一個(gè)重點(diǎn),我們都知道 UDP 本身沒有連接的概念。在 Android 端應(yīng)用 UDP 和 TCP 的場(chǎng)景是一個(gè)手機(jī)連接另一個(gè)手機(jī)的熱點(diǎn),二者處在同一局域網(wǎng)中。在二者并不知道對(duì)方的存在時(shí),怎么才能發(fā)現(xiàn)彼此呢?

通過心跳包的方式,雙方都每隔一段時(shí)間發(fā)一個(gè) UDP 包,如果對(duì)方接收到了,那就能知道對(duì)方的 ip,建立起通信了。

    private static final long TIME_OUT = 120 * 1000;
    private static final long HEARTBEAT_MESSAGE_DURATION = 10 * 1000;
    /**
     * 啟動(dòng)心跳,timer 間隔十秒
     */
    private void startHeartbeatTimer() {
        timer = new HeartbeatTimer();
        timer.setOnScheduleListener(new HeartbeatTimer.OnScheduleListener() {
            @Override
            public void onSchedule() {
                Log.d(TAG, "timer is onSchedule...");
                long duration = System.currentTimeMillis() - lastReceiveTime;
                Log.d(TAG, "duration:" + duration);
                if (duration > TIME_OUT) {//若超過兩分鐘都沒收到我的心跳包,則認(rèn)為對(duì)方不在線。
                    Log.d(TAG, "超時(shí),對(duì)方已經(jīng)下線");
                    // 刷新時(shí)間,重新進(jìn)入下一個(gè)心跳周期
                    lastReceiveTime = System.currentTimeMillis();
                } else if (duration > HEARTBEAT_MESSAGE_DURATION) {//若超過十秒他沒收到我的心跳包,則重新發(fā)一個(gè)。
                    String string = "hello,this is a heartbeat message";
                    sendMessage(string);
                }
            }

        });
        timer.startTimer(0, 1000 * 10);
    }

這段心跳的目的就是每隔十秒通過 sendMessage 發(fā)送一個(gè)消息,看看對(duì)方能不能收到。若對(duì)方收到消息,則刷新下 lastReceiveTime 的時(shí)間。

這里我每隔十秒向?qū)Ψ桨l(fā)送了一個(gè)字符串。

    private static final String BROADCAST_IP = "192.168.43.255";
    /**
     * 發(fā)送心跳包
     *
     * @param message
     */
    public void sendMessage(final String message) {
        mThreadPool.execute(new Runnable() {
            @Override
            public void run() {
                try {
                    InetAddress targetAddress = InetAddress.getByName(BROADCAST_IP);

                    DatagramPacket packet = new DatagramPacket(message.getBytes(), message.length(), targetAddress, CLIENT_PORT);

                    client.send(packet);

                    Log.d(TAG, "數(shù)據(jù)發(fā)送成功");

                } catch (UnknownHostException e) {
                    e.printStackTrace();
                } catch (IOException e) {
                    e.printStackTrace();
                }

            }
        });
    }

這里就是發(fā)送一個(gè)消息的代碼。最初在填寫 DatagramPacket 的參數(shù)之時(shí),我有一個(gè)疑問,那個(gè) targetAddress 其實(shí)是自己的 ip 地址。問題來了,我填寫了自己的 ip 地址和對(duì)方的端口,怎么可能找得到對(duì)方呢?你可能有一個(gè)疑惑 "192.168.43.255" 這個(gè)自己的 ip 地址是怎么來的,為什么要這么定義?

首先 android 手機(jī)開啟熱點(diǎn),可以理解成一個(gè)網(wǎng)關(guān),有一個(gè)默認(rèn)的 ip 地址:"192.168.43.1"

這個(gè) ip 地址不是我瞎編的一個(gè),在 Android 源碼之中,就是這么定義的:

WifiStateMachine

                        ifcg = mNwService.getInterfaceConfig(intf);
                        if (ifcg != null) {
                            /* IP/netmask: 192.168.43.1/255.255.255.0 */
                            ifcg.setLinkAddress(new LinkAddress(
                                    NetworkUtils.numericToInetAddress("192.168.43.1"), 24));
                            ifcg.setInterfaceUp();

                            mNwService.setInterfaceConfig(intf, ifcg);
                        }

所以我是知道所謂打開熱點(diǎn)一方的 ip 地址,而 UDP 發(fā)送消息時(shí)還有一個(gè)特性,就是發(fā)出去的消息,處在整個(gè)網(wǎng)關(guān)的設(shè)備是都可以接收到的,所以我自己的 ip 地址就定為了 "192.168.43.255",所以這個(gè) ip 地址和 "192.168.43.1" 在同一網(wǎng)關(guān)中,你發(fā)送的消息,它是可以收到的。

至于怎么判斷兩個(gè) ip 地址是否處在同一網(wǎng)段中:

判斷兩個(gè)IP大小及是否在同一個(gè)網(wǎng)段中

來做一個(gè)階段總結(jié):

首先我們創(chuàng)建了一個(gè)發(fā)送端 DatagramSocket,啟動(dòng)了一個(gè)心跳程序,每間隔一段時(shí)間發(fā)送一個(gè)心跳包。

因?yàn)槲抑罒狳c(diǎn)方的 ip 地址是默認(rèn)的 "192.168.43.1",并且 UDP 的特性就是發(fā)送的消息同一網(wǎng)段的設(shè)備都可以收到。所以發(fā)送方的 ip 地址定為了與熱點(diǎn)一方處在同一網(wǎng)段的 "192.168.43.255"。

事件與數(shù)據(jù)

事件與數(shù)據(jù)這兩個(gè)模塊與業(yè)務(wù)就緊密相關(guān)了。

先來說數(shù)據(jù),雙方發(fā)送的數(shù)據(jù)格式你們可以隨意定義,當(dāng)然我覺得還是定義成常規(guī)的 Json 格式就好。其中可以包含一些關(guān)鍵的事件字段:比如廣播心跳包、收到心跳包給對(duì)方上線的應(yīng)答包、超時(shí)的下線包、以及各種業(yè)務(wù)相關(guān)的數(shù)據(jù)等等。

當(dāng)然發(fā)送數(shù)據(jù)時(shí)是轉(zhuǎn)換成二進(jìn)制數(shù)組發(fā)送的。發(fā)送中文字符、圖片等都沒有問題,但是可能有一些細(xì)節(jié)需要注意,隨時(shí) google 一下就好了。

再來說下事件:

與業(yè)務(wù)無關(guān)的事件有哪些?

比如:
DatagramSocket.send 方法之后就是發(fā)送數(shù)據(jù)成功的事件;

DatagramSocket.receive 方法之后是數(shù)據(jù)接收成功的事件;

在心跳包發(fā)送一段時(shí)間,仍沒有接到回信時(shí),是連接超時(shí)的事件;

與業(yè)務(wù)相關(guān)的事件就和我們上文提到的數(shù)據(jù)類型有關(guān)了,設(shè)備上線,心跳包回應(yīng)等等。

事件又如何發(fā)送出去,通知到各個(gè)頁面呢?用 Listener、或者其他事件總線的三方庫(kù)都沒問題,看你自己選擇了。

處理接收的消息

    /**
     * 處理接受到的消息
     */
    private void receiveMessage() {
        while (isThreadRunning) {
            try {
                if (client != null) {
                    client.receive(receivePacket);
                }
                lastReceiveTime = System.currentTimeMillis();
                Log.d(TAG, "receive packet success...");
            } catch (IOException e) {
                Log.e(TAG, "UDP數(shù)據(jù)包接收失?。【€程停止");
                stopUDPSocket();
                e.printStackTrace();
                return;
            }

            if (receivePacket == null || receivePacket.getLength() == 0) {
                Log.e(TAG, "無法接收UDP數(shù)據(jù)或者接收到的UDP數(shù)據(jù)為空");
                continue;
            }

            String strReceive = new String(receivePacket.getData(), 0, receivePacket.getLength());
            Log.d(TAG, strReceive + " from " + receivePacket.getAddress().getHostAddress() + ":" + receivePacket.getPort());

            //解析接收到的 json 信息

            // 每次接收完UDP數(shù)據(jù)后,重置長(zhǎng)度。否則可能會(huì)導(dǎo)致下次收到數(shù)據(jù)包被截?cái)唷?            if (receivePacket != null) {
                receivePacket.setLength(BUFFER_LENGTH);
            }
        }
    }

處理接收消息時(shí),有幾個(gè)值得注意的點(diǎn):

  1. receive 方法是阻塞的,沒收到數(shù)據(jù)包時(shí)會(huì)一直阻塞,所以要放到子線程中;
  2. 每次接收到消息之后,重新調(diào)用 receivePacket.setLength;
  3. 收到消息刷新lastReceiveTime的值,暫停心跳包的發(fā)送;

處理收到的數(shù)據(jù)具體在業(yè)務(wù)上就是剛才我們談的發(fā)送數(shù)據(jù)的問題,視業(yè)務(wù)而定。

“用戶”的概念

上文已經(jīng)談過了 UDP 的特性,假如一個(gè)手機(jī)已經(jīng)開啟了熱點(diǎn),若多個(gè)手機(jī)與他相連接,則多個(gè)手機(jī)發(fā)送的消息它都可以收到。如果發(fā)送方的端口與接收方的端口相同的話,甚至自己發(fā)的消息,自己都可以收到。這就很尷尬了,也就是說我們既要剔除自己發(fā)給自己的消息,也得區(qū)分不同手機(jī)發(fā)來的消息,這個(gè)時(shí)候就理應(yīng)有一個(gè)“用戶”的概念。

創(chuàng)建 User 對(duì)象,有哪些屬性可以看自己的業(yè)務(wù),本文的例子就有 ip、imei、以及 softversion。

    /**
     * 創(chuàng)建本地用戶信息
     */
    private void createUser() {
        if (localUser == null) {
            localUser = new Users();
        }
        if (remoteUser == null) {
            remoteUser = new Users();
        }

        localUser.setImei(DeviceUtil.getDeviceId(mContext));
        localUser.setSoftVersion(DeviceUtil.getPackageVersionCode(mContext));

        if (WifiUtil.getInstance(mContext).isWifiApEnabled()) {// 判斷當(dāng)前是否是開啟熱點(diǎn)方
            localUser.setIp("192.168.43.1");
        } else {// 當(dāng)前是開啟 wifi 方
            localUser.setIp(WifiUtil.getInstance(mContext).getLocalIPAddress());
            remoteUser.setIp(WifiUtil.getInstance(mContext).getServerIPAddress());
        }
    }
    /**
     * <p><b>IMEI.</b></p> Returns the unique device ID, for example, the IMEI for GSM and the MEID
     * or ESN for CDMA phones. Return null if device ID is not available.
     * <p>
     * Requires Permission: READ_PHONE_STATE
     *
     * @param context
     * @return
     */
    public synchronized static String getDeviceId(Context context) {
        if (context == null) {
            return "";
        }

        String imei = "";

        try {
            TelephonyManager tm = (TelephonyManager) context.getSystemService(Context.TELEPHONY_SERVICE);
            if (tm == null || TextUtils.isEmpty(tm.getDeviceId())) {
                // 雙卡雙待需要通過phone1和phone2獲取imei,默認(rèn)取phone1的imei。
                tm = (TelephonyManager) context.getSystemService("phone1");
            }

            if (tm != null) {
                imei = tm.getDeviceId();
            }
        } catch (SecurityException e) {
            e.printStackTrace();
        }


        return imei;
    }

這里就不將所有的代碼展開來看了。如果有了手機(jī)的 imei 號(hào),那很容易就可以來做身份的區(qū)分,你既可以區(qū)分不同的發(fā)送方,也可以剔除掉自己發(fā)給自己的消息。當(dāng)然如果需要更多的信息,可以按照自己的業(yè)務(wù)區(qū)分,將這些信息作為發(fā)送的 messge,通過 Socket 發(fā)送。

寫在后面:
到現(xiàn)在開始本文的大部分內(nèi)容都已經(jīng)介紹完成,有的同學(xué)可能會(huì)發(fā)問,你要用一個(gè)心跳來維持一個(gè)假的“長(zhǎng)連接”,使用起來比較麻煩,而且還可能忍受 UDP 造成的丟包的痛苦,為什么不選擇 TCP 呢?問得好,其實(shí)這個(gè)版本是當(dāng)時(shí)做的第一個(gè)版本,之后就使用 TCP+UDP 的方式來完成這個(gè)模塊了,下一篇文章再來看看加上 TCP 的改進(jìn)版吧。

本文例子的所有代碼在這里:

github 連接在這里 UDPSocketDemo

最后編輯于
?著作權(quán)歸作者所有,轉(zhuǎn)載或內(nèi)容合作請(qǐng)聯(lián)系作者
【社區(qū)內(nèi)容提示】社區(qū)部分內(nèi)容疑似由AI輔助生成,瀏覽時(shí)請(qǐng)結(jié)合常識(shí)與多方信息審慎甄別。
平臺(tái)聲明:文章內(nèi)容(如有圖片或視頻亦包括在內(nèi))由作者上傳并發(fā)布,文章內(nèi)容僅代表作者本人觀點(diǎn),簡(jiǎn)書系信息發(fā)布平臺(tái),僅提供信息存儲(chǔ)服務(wù)。

相關(guān)閱讀更多精彩內(nèi)容

  • 個(gè)人認(rèn)為,Goodboy1881先生的TCP /IP 協(xié)議詳解學(xué)習(xí)博客系列博客是一部非常精彩的學(xué)習(xí)筆記,這雖然只是...
    貳零壹柒_fc10閱讀 5,200評(píng)論 0 8
  • 1.這篇文章不是本人原創(chuàng)的,只是個(gè)人為了對(duì)這部分知識(shí)做一個(gè)整理和系統(tǒng)的輸出而編輯成的,在此鄭重地向本文所引用文章的...
    SOMCENT閱讀 13,383評(píng)論 6 174
  • 簡(jiǎn)介 用簡(jiǎn)單的話來定義tcpdump,就是:dump the traffic on a network,根據(jù)使用者...
    保川閱讀 6,088評(píng)論 1 13
  • Teredo 原理概述 http://www.ipv6bbs.cn/thread-144-1-1.html (出處...
    我是葉問小盆友閱讀 2,364評(píng)論 0 1
  • 一葉一世界,一佛一如來,靜心禪定。
    木朵兒閱讀 3,968評(píng)論 0 0

友情鏈接更多精彩內(nèi)容