欢迎您访问程序员文章站本站旨在为大家提供分享程序员计算机编程知识!
您现在的位置是: 首页  >  移动技术

Android开发中一套完整的 Socket 解决方案

程序员文章站 2023-02-26 13:33:36
android开发中一套完整的 socket 解决方案。 文中介绍了在 android 端,一个完整的 udp 模块应该考虑哪些方面。当然了文中最后也提到了,udp 的使用本身就...

android开发中一套完整的 socket 解决方案。

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

整体步骤流程

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

发送 udp 广播,大家都知道 udp 广播的特性是整个网段的设备都可以收到这个消息。 接收方收到了 udp 的广播,将自己的 ip 地址,和双方约定的端口号,回复给 udp 的发送方。 发送方拿到了对方的 ip 地址以及端口号,就可以发起 tcp 请求了,建立 tcp 连接。 保持一个 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 密集型啊,来开启不同的线程通道。这个就涉及线程的知识了。