Android开发中一套完整的 Socket 解决方案
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 模块
接下来 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 密集型啊,来开启不同的线程通道。这个就涉及线程的知识了。
下一篇: Android编程开发中常见混淆机制解析