Android 一套完整的 Socket 解决方案

Android 一套完整的 Socket 解决方案

本文原创,转载请注明出处。
欢迎关注我的 简书 ,关注我的专题 Android Class 我会长期坚持为大家收录简书上高质量的 Android 相关博文。

项目地址,喜欢点一个 star:

AndroidSocket

写在前面:

在上上周的时候,写了一篇文章:

在 Android 上,一个完整的 UDP 通信模块应该是怎样的?

文中介绍了在 Android 端,一个完整的 UDP 模块应该考虑哪些方面。当然了文中最后也提到了,UDP 的使用本身就有一些局限性,比如发送数据的大小有限制,属于不可靠协议,可能丢包。而且它是一对多发送的协议等等...如果能将这个模块能加入 TCP Socket 补充,那就比较完美解决了 Android 上端到端的通信。下面就来看看怎么去做。

整体步骤流程

先来说一下整体的步骤思路吧:

  1. 发送 UDP 广播,大家都知道 UDP 广播的特性是整个网段的设备都可以收到这个消息。
  2. 接收方收到了 UDP 的广播,将自己的 ip 地址,和双方约定的端口号,回复给 UDP 的发送方。
  3. 发送方拿到了对方的 ip 地址以及端口号,就可以发起 TCP 请求了,建立 TCP 连接。
  4. 保持一个 TCP 心跳,如果发现对方不在了,超时重复 1 步骤,重新建立联系。

整体的步骤就和上述的一样,下面用代码展开:

搭建 UDP 模块

    public UDPSocket(Context context) {

        this.mContext = context;

        int cpuNumbers = Runtime.getRuntime().availableProcessors();
        // 根据CPU数目初始化线程池
        mThreadPool = Executors.newFixedThreadPool(cpuNumbers * Config.POOL_SIZE);
        // 记录创建对象时的时间
        lastReceiveTime = System.currentTimeMillis();

        messageReceiveList = new ArrayList<>();

        Log.d(TAG, "创建 UDP 对象");
//        createUser();
    }

首先进行一些初始化操作,准备线程池,记录对象初始的时间等等。

    public void startUDPSocket() {
        if (client != null) return;
        try {
            // 表明这个 Socket 在设置的端口上监听数据。
            client = new DatagramSocket(CLIENT_PORT);
            client.setReuseAddress(true);
            if (receivePacket == null) {
                // 创建接受数据的 packet
                receivePacket = new DatagramPacket(receiveByte, BUFFER_LENGTH);
            }

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

紧接着就创建了真正的一个 UDP Socket 端,DatagramSocket,注意这里传入的端口号 CLIENT_PORT 的意思是这个 DatagramSocket 在此端口号接收消息。

    /**
     * 开启发送数据的线程
     */
    private void startSocketThread() {
        clientThread = new Thread(new Runnable() {
            @Override
            public void run() {
                receiveMessage();
            }
        });
        isThreadRunning = true;
        clientThread.start();
        Log.d(TAG, "开启 UDP 数据接收线程");

        startHeartbeatTimer();
    }

我们都知道 Socket 中要处理数据的发送和接收,并且发送和接收都是阻塞的,应该放在子线程中,这里就开启了一个线程,来处理接收到的 UDP 消息(UDP 模块上一篇文章讲得比较详细了,所以这里就不详细展开了)

    /**
     * 处理接受到的消息
     */
    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数据包接收失败!线程停止");
                stopUDPSocket();
                e.printStackTrace();
                return;
            }

            if (receivePacket == null || receivePacket.getLength() == 0) {
                Log.e(TAG, "无法接收UDP数据或者接收到的UDP数据为空");
                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数据后,重置长度。否则可能会导致下次收到数据包被截断。
            if (receivePacket != null) {
                receivePacket.setLength(BUFFER_LENGTH);
            }
        }
    }

在子线程接收 UDP 数据,并且 notifyMessageReceive 方法通过接口来向外通知消息。

    /**
     * 发送心跳包
     *
     * @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);

                    // 数据发送事件
                    Log.d(TAG, "数据发送成功");

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

            }
        });
    }

接着 startHeartbeatTimer 开启一个心跳线程,每间隔五秒,就去广播一个 UDP 消息。注意这里 getBroadcastAddress 是获取的网段 ip,发送这个 UDP 消息的时候,整个网段的所有设备都可以接收到。

到此为止,我们发送端的 UDP 算是搭建完成了。

搭建 TCP 模块

接下来 TCP 模块该出场了,UDP 发送心跳广播的目的就是找到对应设备的 ip 地址和约定好的端口,所以在 UDP 数据的接收方法里:

    /**
     * 处理 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();
        }
    }

这个方法的目的就是取到对方 UDPServer 端,发给我的 UDP 消息,将它的 ip 地址告诉了我,以及我们提前约定好的端口号。

怎么获得一个设备的 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);
    }

现在拿到了对方的 ip,以及约定好的端口号,终于可以开启一个 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 创建成功...");
            return true;
        } catch (Exception e) {
            e.printStackTrace();
        }
        return false;
    }

当 TCP 客户端成功建立的时候,我们就可以通过 TCP Socket 来发送和接收消息了。

细节处理

接下来就是一些细节处理了,比如我们的 UDP 心跳,当 TCP 建立成功之时,我们要停止 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已经成功建立连接,停止 UDP 的心跳包。
            public void stopHeartbeatTimer() {
                if (timer != null) {
                    timer.exit();
                    timer = null;
                }
    }

对 TCP 连接进行心跳保护:

    /**
     * 启动心跳
     */
    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) {//若超过十五秒都没收到我的心跳包,则认为对方不在线。
                    Log.d(TAG, "tcp ping 超时,对方已经下线");
                    stopTcpConnection();
                    if (mListener != null) {
                        mListener.onFailed(Config.ErrorCode.PING_TCP_TIMEOUT);
                    }
                } else if (duration > HEARTBEAT_MESSAGE_DURATION) {//若超过两秒他没收到我的心跳包,则重新发一个。
                    JSONObject jsonObject = new JSONObject();
                    try {
                        jsonObject.put(Config.MSG, Config.PING);
                    } catch (JSONException e) {
                        e.printStackTrace();
                    }
                    sendTcpMessage(jsonObject.toString());
                }
            }

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

首先会每隔两秒,就给对方发送一个 ping 包,看看对面在不在,如果超过 15 秒还没有回复我,那就说明对方掉线了,关闭我这边的 TCP 端。进入 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;
                    }
                }

当 TCP 连接超时,我就会重新启动 UDP 的广播心跳,寻找等待连接的设备。进入下一个步骤循环。

对于数据传输的格式啊等等细节,这个和业务相关。自己来定就好。

还可以根据自己业务的模式,是 CPU 密集型啊,还是 IO 密集型啊,来开启不同的线程通道。这个就涉及线程的知识了。

项目地址,喜欢点一个 star:

AndroidSocket

©著作权归作者所有,转载或内容合作请联系作者
  • 序言:七十年代末,一起剥皮案震惊了整个滨河市,随后出现的几起案子,更是在滨河造成了极大的恐慌,老刑警刘岩,带你破解...
    沈念sama阅读 194,242评论 5 459
  • 序言:滨河连续发生了三起死亡事件,死亡现场离奇诡异,居然都是意外死亡,警方通过查阅死者的电脑和手机,发现死者居然都...
    沈念sama阅读 81,769评论 2 371
  • 文/潘晓璐 我一进店门,熙熙楼的掌柜王于贵愁眉苦脸地迎上来,“玉大人,你说我怎么就摊上这事。” “怎么了?”我有些...
    开封第一讲书人阅读 141,484评论 0 319
  • 文/不坏的土叔 我叫张陵,是天一观的道长。 经常有香客问我,道长,这世上最难降的妖魔是什么? 我笑而不...
    开封第一讲书人阅读 52,133评论 1 263
  • 正文 为了忘掉前任,我火速办了婚礼,结果婚礼上,老公的妹妹穿的比我还像新娘。我一直安慰自己,他们只是感情好,可当我...
    茶点故事阅读 61,007评论 4 355
  • 文/花漫 我一把揭开白布。 她就那样静静地躺着,像睡着了一般。 火红的嫁衣衬着肌肤如雪。 梳的纹丝不乱的头发上,一...
    开封第一讲书人阅读 46,080评论 1 272
  • 那天,我揣着相机与录音,去河边找鬼。 笑死,一个胖子当着我的面吹牛,可吹牛的内容都是我干的。 我是一名探鬼主播,决...
    沈念sama阅读 36,496评论 3 381
  • 文/苍兰香墨 我猛地睁开眼,长吁一口气:“原来是场噩梦啊……” “哼!你这毒妇竟也来了?” 一声冷哼从身侧响起,我...
    开封第一讲书人阅读 35,190评论 0 253
  • 序言:老挝万荣一对情侣失踪,失踪者是张志新(化名)和其女友刘颖,没想到半个月后,有当地人在树林里发现了一具尸体,经...
    沈念sama阅读 39,464评论 1 290
  • 正文 独居荒郊野岭守林人离奇死亡,尸身上长有42处带血的脓包…… 初始之章·张勋 以下内容为张勋视角 年9月15日...
    茶点故事阅读 34,549评论 2 309
  • 正文 我和宋清朗相恋三年,在试婚纱的时候发现自己被绿了。 大学时的朋友给我发了我未婚夫和他白月光在一起吃饭的照片。...
    茶点故事阅读 36,330评论 1 326
  • 序言:一个原本活蹦乱跳的男人离奇死亡,死状恐怖,灵堂内的尸体忽然破棺而出,到底是诈尸还是另有隐情,我是刑警宁泽,带...
    沈念sama阅读 32,205评论 3 312
  • 正文 年R本政府宣布,位于F岛的核电站,受9级特大地震影响,放射性物质发生泄漏。R本人自食恶果不足惜,却给世界环境...
    茶点故事阅读 37,567评论 3 298
  • 文/蒙蒙 一、第九天 我趴在偏房一处隐蔽的房顶上张望。 院中可真热闹,春花似锦、人声如沸。这庄子的主人今日做“春日...
    开封第一讲书人阅读 28,889评论 0 17
  • 文/苍兰香墨 我抬头看了看天上的太阳。三九已至,却和暖如春,着一层夹袄步出监牢的瞬间,已是汗流浃背。 一阵脚步声响...
    开封第一讲书人阅读 30,160评论 1 250
  • 我被黑心中介骗来泰国打工, 没想到刚下飞机就差点儿被人妖公主榨干…… 1. 我叫王不留,地道东北人。 一个月前我还...
    沈念sama阅读 41,475评论 2 341
  • 正文 我出身青楼,却偏偏与公主长得像,于是被迫代替她去往敌国和亲。 传闻我的和亲对象是个残疾皇子,可洞房花烛夜当晚...
    茶点故事阅读 40,650评论 2 335

推荐阅读更多精彩内容

  • 1.这篇文章不是本人原创的,只是个人为了对这部分知识做一个整理和系统的输出而编辑成的,在此郑重地向本文所引用文章的...
    SOMCENT阅读 13,028评论 6 174
  • 个人认为,Goodboy1881先生的TCP /IP 协议详解学习博客系列博客是一部非常精彩的学习笔记,这虽然只是...
    贰零壹柒_fc10阅读 5,042评论 0 8
  • 最近在学习Python看了一篇文章写得不错,是在脚本之家里的,原文如下,很有帮助: 一、网络知识的一些介绍 soc...
    qtruip阅读 2,670评论 0 6
  • 11.1 引言 UDP是一个简单的面向数据报的运输层协议:进程的每个输出操作都正好产生一个UDP数据报,并组装成一...
    张芳涛阅读 2,762评论 1 6
  • Chapter3 分支 参考自 https://git-scm.com/book/zh/v1/Git-%E5%88...
    董哒哒阅读 449评论 0 0