Android 一套完整的 Socket 解决方案

转载 2018年01月04日 00:00:00

?wxfrom=5&wx_lazy=1

作者 | MeloDev

地址 | http://www.jianshu.com/p/61de9478c9aa

声明 | 本文是 MeloDev 原创,已获授权发布,未经原作者允许请勿转载



写在前面

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

https://github.com/itsMelo/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:

https://github.com/itsMelo/AndroidSocket


?


与之相关

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



?


Android 一套完整的 Socket 解决方案

Android 一套完整的 Socket 解决方案项目地址,喜欢点一个 star:AndroidSocket写在前面:在上上周的时候,写了一篇文章:在 Android 上,一个完整的 UDP 通信模块...
  • MeloDev
  • MeloDev
  • 2017年11月30日 20:59
  • 293

【通用开发框架】一套完整的Android通用开发框架

MVP模式 MVP 简介 Android MVP Sample,MVP+Retrofit+RxJava实践小结 github地址 https://gith...
  • ourpush
  • ourpush
  • 2016年11月28日 21:54
  • 419

一套完整的测试应该由五个阶段组成

一套完整的测试应该由五个阶段组成:      1.测试计划   首先,根据用户需求报告中关于功能要求和性能指标的规格说明书,定义相应的测试需求报告,即制订黑盒测试的最高标准,以后所有的测试工作都...
  • wei_zhi
  • wei_zhi
  • 2016年02月29日 16:10
  • 2445

PHP实现系统编程(一) --- 网络Socket及IO多路复用

一直以来,PHP很少用于socket编程,毕竟是一门脚本语言,效率会成为很大的瓶颈,但是不能说PHP就无法用于socket编程,也不能说PHP的socket编程性能就有多么的低,例如知名的一款PHP ...
  • zhang197093
  • zhang197093
  • 2017年08月18日 17:24
  • 954

在牛客网看到这样的一个选择题,给出自己的见解

题目是: 以下程序输出是____。 1 2 3 4 5 6 7 8 9 10 #include   using namespace std;  int main(void)  {...
  • Sanlence
  • Sanlence
  • 2015年04月12日 00:14
  • 808

BI只是一种解决方案

商业智能描述了一系列的概念和方法,提供使企业迅速分析数据的技术和方法,包括收集、管理和分析数据,将数据转化为有用的信息并根据需要进行分发,从而辅助商业决策的制定。 每个企业面临的数据环境、业务内...
  • dongzhumao86
  • dongzhumao86
  • 2014年03月17日 16:35
  • 786

抓取Android平台数据包之tcpdump 工具的使用

最近有个简化联网统计包的需求,需要挖掘封装JAR包的上报链接。 jar包是混淆过的。所以也不能直接看到链接。 于是就只能在联网的时候抓取上报链接。苦于网上没有一个完整的教程。本着CSDN的互助精神...
  • kakathya
  • kakathya
  • 2016年01月07日 11:06
  • 532

c++继承应用->写一套代码跨越IOS和Android两个平台

1,MyTest.h #ifndef _MyTest_ #define _MyTest_ class MyTestImpl; class MyTest { public: MyTest(); ...
  • themagickeyjianan
  • themagickeyjianan
  • 2014年07月26日 18:30
  • 1854

android动态屏幕适配(不需要多套图,多布局)

1.工具类: public class SupportDisplay { /** * 基准屏横 */ private static final float BASIC_SCREEN_W...
  • u012483116
  • u012483116
  • 2016年07月22日 15:54
  • 1110

android网络通信之socket教程实例汇总

一、socket基础 1、Socket通讯机制(详细),如何将socket通信的客户端与服务器 http://www.eoeandroid.com/thread-61727-1-1.html ...
  • qian_xiao_lj
  • qian_xiao_lj
  • 2016年03月04日 16:01
  • 2635
内容举报
返回顶部
收藏助手
不良信息举报
您举报文章:Android 一套完整的 Socket 解决方案
举报原因:
原因补充:

(最多只允许输入30个字)