Android 一套完整的 Socket 解決方案

Android 一套完整的 Socket 解決方案

本文原創(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)博文抬驴。

項(xiàng)目地址,喜歡點(diǎn)一個(gè) star:

AndroidSocket

寫在前面:

在上上周的時(shí)候缆巧,寫了一篇文章:

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

文中介紹了在 Android 端陕悬,一個(gè)完整的 UDP 模塊應(yīng)該考慮哪些方面题暖。當(dāng)然了文中最后也提到了,UDP 的使用本身就有一些局限性捉超,比如發(fā)送數(shù)據(jù)的大小有限制胧卤,屬于不可靠協(xié)議,可能丟包拼岳。而且它是一對(duì)多發(fā)送的協(xié)議等等...如果能將這個(gè)模塊能加入 TCP Socket 補(bǔ)充枝誊,那就比較完美解決了 Android 上端到端的通信。下面就來看看怎么去做惜纸。

整體步驟流程

先來說一下整體的步驟思路吧:

  1. 發(fā)送 UDP 廣播侧啼,大家都知道 UDP 廣播的特性是整個(gè)網(wǎng)段的設(shè)備都可以收到這個(gè)消息。
  2. 接收方收到了 UDP 的廣播堪簿,將自己的 ip 地址痊乾,和雙方約定的端口號(hào),回復(fù)給 UDP 的發(fā)送方椭更。
  3. 發(fā)送方拿到了對(duì)方的 ip 地址以及端口號(hào)哪审,就可以發(fā)起 TCP 請(qǐng)求了,建立 TCP 連接虑瀑。
  4. 保持一個(gè) TCP 心跳湿滓,如果發(fā)現(xiàn)對(duì)方不在了,超時(shí)重復(fù) 1 步驟舌狗,重新建立聯(lián)系叽奥。

整體的步驟就和上述的一樣,下面用代碼展開:

搭建 UDP 模塊

    public UDPSocket(Context context) {

        this.mContext = context;

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

        messageReceiveList = new ArrayList<>();

        Log.d(TAG, "創(chuàng)建 UDP 對(duì)象");
//        createUser();
    }

首先進(jìn)行一些初始化操作痛侍,準(zhǔn)備線程池朝氓,記錄對(duì)象初始的時(shí)間等等。

    public void startUDPSocket() {
        if (client != null) return;
        try {
            // 表明這個(gè) Socket 在設(shè)置的端口上監(jiān)聽數(shù)據(jù)。
            client = new DatagramSocket(CLIENT_PORT);
            client.setReuseAddress(true);
            if (receivePacket == null) {
                // 創(chuàng)建接受數(shù)據(jù)的 packet
                receivePacket = new DatagramPacket(receiveByte, BUFFER_LENGTH);
            }

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

緊接著就創(chuàng)建了真正的一個(gè) UDP Socket 端赵哲,DatagramSocket待德,注意這里傳入的端口號(hào) CLIENT_PORT 的意思是這個(gè) DatagramSocket 在此端口號(hào)接收消息。

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

        startHeartbeatTimer();
    }

我們都知道 Socket 中要處理數(shù)據(jù)的發(fā)送和接收枫夺,并且發(fā)送和接收都是阻塞的将宪,應(yīng)該放在子線程中,這里就開啟了一個(gè)線程橡庞,來處理接收到的 UDP 消息(UDP 模塊上一篇文章講得比較詳細(xì)了较坛,所以這里就不詳細(xì)展開了)

    /**
     * 處理接受到的消息
     */
    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(), receivePacket.getOffset(), receivePacket.getLength());
            Log.d(TAG, strReceive + " from " + receivePacket.getAddress().getHostAddress() + ":" + receivePacket.getPort());

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

在子線程接收 UDP 數(shù)據(jù)扼倘,并且 notifyMessageReceive 方法通過接口來向外通知消息确封。

    /**
     * 發(fā)送心跳包
     *
     * @param message
     */
    public void sendMessage(final String message) {
        mThreadPool.execute(new Runnable() {
            @Override
            public void run() {
                try {
                    BROADCAST_IP = WifiUtil.getBroadcastAddress();
                    Log.d(TAG, "BROADCAST_IP:" + BROADCAST_IP);
                    InetAddress targetAddress = InetAddress.getByName(BROADCAST_IP);

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

                    client.send(packet);

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

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

            }
        });
    }

接著 startHeartbeatTimer 開啟一個(gè)心跳線程,每間隔五秒再菊,就去廣播一個(gè) UDP 消息爪喘。注意這里 getBroadcastAddress 是獲取的網(wǎng)段 ip,發(fā)送這個(gè) UDP 消息的時(shí)候纠拔,整個(gè)網(wǎng)段的所有設(shè)備都可以接收到秉剑。

到此為止,我們發(fā)送端的 UDP 算是搭建完成了稠诲。

搭建 TCP 模塊

接下來 TCP 模塊該出場(chǎng)了侦鹏,UDP 發(fā)送心跳廣播的目的就是找到對(duì)應(yīng)設(shè)備的 ip 地址和約定好的端口,所以在 UDP 數(shù)據(jù)的接收方法里:

    /**
     * 處理 udp 收到的消息
     *
     * @param message
     */
    private void handleUdpMessage(String message) {
        try {
            JSONObject jsonObject = new JSONObject(message);
            String ip = jsonObject.optString(Config.TCP_IP);
            String port = jsonObject.optString(Config.TCP_PORT);
            if (!TextUtils.isEmpty(ip) && !TextUtils.isEmpty(port)) {
                startTcpConnection(ip, port);
            }
        } catch (JSONException e) {
            e.printStackTrace();
        }
    }

這個(gè)方法的目的就是取到對(duì)方 UDPServer 端臀叙,發(fā)給我的 UDP 消息略水,將它的 ip 地址告訴了我,以及我們提前約定好的端口號(hào)劝萤。

怎么獲得一個(gè)設(shè)備的 ip 呢渊涝?

    public String getLocalIPAddress() {
        WifiInfo wifiInfo = mWifiManager.getConnectionInfo();
        return intToIp(wifiInfo.getIpAddress());
    }
    private static String intToIp(int i) {
        return (i & 0xFF) + "." + ((i >> 8) & 0xFF) + "." + ((i >> 16) & 0xFF) + "."
                + ((i >> 24) & 0xFF);
    }

現(xiàn)在拿到了對(duì)方的 ip,以及約定好的端口號(hào)床嫌,終于可以開啟一個(gè) TCP 客戶端了跨释。

    private boolean startTcpConnection(final String ip, final int port) {
        try {
            if (mSocket == null) {
                mSocket = new Socket(ip, port);
                mSocket.setKeepAlive(true);
                mSocket.setTcpNoDelay(true);
                mSocket.setReuseAddress(true);
            }
            InputStream is = mSocket.getInputStream();
            br = new BufferedReader(new InputStreamReader(is));
            OutputStream os = mSocket.getOutputStream();
            pw = new PrintWriter(new BufferedWriter(new OutputStreamWriter(os)), true);
            Log.d(TAG, "tcp 創(chuàng)建成功...");
            return true;
        } catch (Exception e) {
            e.printStackTrace();
        }
        return false;
    }

當(dāng) TCP 客戶端成功建立的時(shí)候,我們就可以通過 TCP Socket 來發(fā)送和接收消息了厌处。

細(xì)節(jié)處理

接下來就是一些細(xì)節(jié)處理了鳖谈,比如我們的 UDP 心跳,當(dāng) TCP 建立成功之時(shí)阔涉,我們要停止 UDP 的心跳:

                if (startTcpConnection(ip, Integer.valueOf(port))) {// 嘗試建立 TCP 連接
                    if (mListener != null) {
                        mListener.onSuccess();
                    }
                    startReceiveTcpThread();
                    startHeartbeatTimer();
                } else {
                    if (mListener != null) {
                        mListener.onFailed(Config.ErrorCode.CREATE_TCP_ERROR);
                    }
                }

            // TCP已經(jīng)成功建立連接缆娃,停止 UDP 的心跳包捷绒。
            public void stopHeartbeatTimer() {
                if (timer != null) {
                    timer.exit();
                    timer = null;
                }
    }

對(duì) TCP 連接進(jìn)行心跳保護(hù):

    /**
     * 啟動(dòng)心跳
     */
    private void startHeartbeatTimer() {
        if (timer == null) {
            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, "tcp ping 超時(shí)疙驾,對(duì)方已經(jīng)下線");
                    stopTcpConnection();
                    if (mListener != null) {
                        mListener.onFailed(Config.ErrorCode.PING_TCP_TIMEOUT);
                    }
                } else if (duration > HEARTBEAT_MESSAGE_DURATION) {//若超過兩秒他沒收到我的心跳包凶伙,則重新發(fā)一個(gè)郭毕。
                    JSONObject jsonObject = new JSONObject();
                    try {
                        jsonObject.put(Config.MSG, Config.PING);
                    } catch (JSONException e) {
                        e.printStackTrace();
                    }
                    sendTcpMessage(jsonObject.toString());
                }
            }

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

首先會(huì)每隔兩秒,就給對(duì)方發(fā)送一個(gè) ping 包函荣,看看對(duì)面在不在显押,如果超過 15 秒還沒有回復(fù)我,那就說明對(duì)方掉線了傻挂,關(guān)閉我這邊的 TCP 端乘碑。進(jìn)入 onFailed 方法。

                @Override
                public void onFailed(int errorCode) {// tcp 異常處理
                    switch (errorCode) {
                        case Config.ErrorCode.CREATE_TCP_ERROR:
                            break;
                        case Config.ErrorCode.PING_TCP_TIMEOUT:
                            udpSocket.startHeartbeatTimer();
                            tcpSocket = null;
                            break;
                    }
                }

當(dāng) TCP 連接超時(shí)金拒,我就會(huì)重新啟動(dòng) UDP 的廣播心跳兽肤,尋找等待連接的設(shè)備。進(jìn)入下一個(gè)步驟循環(huán)绪抛。

對(duì)于數(shù)據(jù)傳輸?shù)母袷桨〉鹊燃?xì)節(jié)资铡,這個(gè)和業(yè)務(wù)相關(guān)。自己來定就好幢码。

還可以根據(jù)自己業(yè)務(wù)的模式笤休,是 CPU 密集型啊,還是 IO 密集型啊症副,來開啟不同的線程通道店雅。這個(gè)就涉及線程的知識(shí)了。

項(xiàng)目地址贞铣,喜歡點(diǎn)一個(gè) star:

AndroidSocket

?著作權(quán)歸作者所有,轉(zhuǎn)載或內(nèi)容合作請(qǐng)聯(lián)系作者
  • 序言:七十年代末闹啦,一起剝皮案震驚了整個(gè)濱河市,隨后出現(xiàn)的幾起案子辕坝,更是在濱河造成了極大的恐慌亥揖,老刑警劉巖,帶你破解...
    沈念sama閱讀 219,589評(píng)論 6 508
  • 序言:濱河連續(xù)發(fā)生了三起死亡事件圣勒,死亡現(xiàn)場(chǎng)離奇詭異费变,居然都是意外死亡,警方通過查閱死者的電腦和手機(jī)圣贸,發(fā)現(xiàn)死者居然都...
    沈念sama閱讀 93,615評(píng)論 3 396
  • 文/潘曉璐 我一進(jìn)店門挚歧,熙熙樓的掌柜王于貴愁眉苦臉地迎上來,“玉大人吁峻,你說我怎么就攤上這事滑负≡谡牛” “怎么了?”我有些...
    開封第一講書人閱讀 165,933評(píng)論 0 356
  • 文/不壞的土叔 我叫張陵矮慕,是天一觀的道長(zhǎng)帮匾。 經(jīng)常有香客問我,道長(zhǎng)痴鳄,這世上最難降的妖魔是什么瘟斜? 我笑而不...
    開封第一講書人閱讀 58,976評(píng)論 1 295
  • 正文 為了忘掉前任,我火速辦了婚禮痪寻,結(jié)果婚禮上螺句,老公的妹妹穿的比我還像新娘。我一直安慰自己橡类,他們只是感情好蛇尚,可當(dāng)我...
    茶點(diǎn)故事閱讀 67,999評(píng)論 6 393
  • 文/花漫 我一把揭開白布。 她就那樣靜靜地躺著顾画,像睡著了一般取劫。 火紅的嫁衣襯著肌膚如雪。 梳的紋絲不亂的頭發(fā)上研侣,一...
    開封第一講書人閱讀 51,775評(píng)論 1 307
  • 那天谱邪,我揣著相機(jī)與錄音,去河邊找鬼义辕。 笑死虾标,一個(gè)胖子當(dāng)著我的面吹牛,可吹牛的內(nèi)容都是我干的灌砖。 我是一名探鬼主播璧函,決...
    沈念sama閱讀 40,474評(píng)論 3 420
  • 文/蒼蘭香墨 我猛地睜開眼,長(zhǎng)吁一口氣:“原來是場(chǎng)噩夢(mèng)啊……” “哼基显!你這毒婦竟也來了蘸吓?” 一聲冷哼從身側(cè)響起,我...
    開封第一講書人閱讀 39,359評(píng)論 0 276
  • 序言:老撾萬榮一對(duì)情侶失蹤撩幽,失蹤者是張志新(化名)和其女友劉穎库继,沒想到半個(gè)月后,有當(dāng)?shù)厝嗽跇淞掷锇l(fā)現(xiàn)了一具尸體窜醉,經(jīng)...
    沈念sama閱讀 45,854評(píng)論 1 317
  • 正文 獨(dú)居荒郊野嶺守林人離奇死亡宪萄,尸身上長(zhǎng)有42處帶血的膿包…… 初始之章·張勛 以下內(nèi)容為張勛視角 年9月15日...
    茶點(diǎn)故事閱讀 38,007評(píng)論 3 338
  • 正文 我和宋清朗相戀三年,在試婚紗的時(shí)候發(fā)現(xiàn)自己被綠了榨惰。 大學(xué)時(shí)的朋友給我發(fā)了我未婚夫和他白月光在一起吃飯的照片拜英。...
    茶點(diǎn)故事閱讀 40,146評(píng)論 1 351
  • 序言:一個(gè)原本活蹦亂跳的男人離奇死亡,死狀恐怖琅催,靈堂內(nèi)的尸體忽然破棺而出居凶,到底是詐尸還是另有隱情虫给,我是刑警寧澤,帶...
    沈念sama閱讀 35,826評(píng)論 5 346
  • 正文 年R本政府宣布侠碧,位于F島的核電站抹估,受9級(jí)特大地震影響,放射性物質(zhì)發(fā)生泄漏弄兜。R本人自食惡果不足惜药蜻,卻給世界環(huán)境...
    茶點(diǎn)故事閱讀 41,484評(píng)論 3 331
  • 文/蒙蒙 一、第九天 我趴在偏房一處隱蔽的房頂上張望挨队。 院中可真熱鬧谷暮,春花似錦蒿往、人聲如沸盛垦。這莊子的主人今日做“春日...
    開封第一講書人閱讀 32,029評(píng)論 0 22
  • 文/蒼蘭香墨 我抬頭看了看天上的太陽腾夯。三九已至,卻和暖如春蔬充,著一層夾襖步出監(jiān)牢的瞬間蝶俱,已是汗流浹背。 一陣腳步聲響...
    開封第一講書人閱讀 33,153評(píng)論 1 272
  • 我被黑心中介騙來泰國打工饥漫, 沒想到剛下飛機(jī)就差點(diǎn)兒被人妖公主榨干…… 1. 我叫王不留榨呆,地道東北人。 一個(gè)月前我還...
    沈念sama閱讀 48,420評(píng)論 3 373
  • 正文 我出身青樓庸队,卻偏偏與公主長(zhǎng)得像积蜻,于是被迫代替她去往敵國和親。 傳聞我的和親對(duì)象是個(gè)殘疾皇子彻消,可洞房花燭夜當(dāng)晚...
    茶點(diǎn)故事閱讀 45,107評(píng)論 2 356

推薦閱讀更多精彩內(nèi)容

  • 1.這篇文章不是本人原創(chuàng)的竿拆,只是個(gè)人為了對(duì)這部分知識(shí)做一個(gè)整理和系統(tǒng)的輸出而編輯成的,在此鄭重地向本文所引用文章的...
    SOMCENT閱讀 13,069評(píng)論 6 174
  • 個(gè)人認(rèn)為宾尚,Goodboy1881先生的TCP /IP 協(xié)議詳解學(xué)習(xí)博客系列博客是一部非常精彩的學(xué)習(xí)筆記丙笋,這雖然只是...
    貳零壹柒_fc10閱讀 5,057評(píng)論 0 8
  • 最近在學(xué)習(xí)Python看了一篇文章寫得不錯(cuò),是在腳本之家里的煌贴,原文如下御板,很有幫助: 一、網(wǎng)絡(luò)知識(shí)的一些介紹 soc...
    qtruip閱讀 2,720評(píng)論 0 6
  • 11.1 引言 UDP是一個(gè)簡(jiǎn)單的面向數(shù)據(jù)報(bào)的運(yùn)輸層協(xié)議:進(jìn)程的每個(gè)輸出操作都正好產(chǎn)生一個(gè)UDP數(shù)據(jù)報(bào)牛郑,并組裝成一...
    張芳濤閱讀 2,816評(píng)論 1 6
  • Chapter3 分支 參考自 https://git-scm.com/book/zh/v1/Git-%E5%88...
    董噠噠閱讀 464評(píng)論 0 0