开发文章

Android编写Socket通迅程序详解

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

整体步骤流程

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

 

  1. 发送 UDP 广播,大家都知道 UDP 广播的特性是整个网段的设备都可以收到这个消息。

  2. 接收方收到了 UDP 的广播,将自己的 ip 地址,和双方约定的端口号,回复给 UDP 的发送方。

  3. 发送方拿到了对方的 ip 地址以及端口号,就可以发起 TCP 请求了,建立 TCP 连接。

  4. 保持一个 TCP 心跳,如果发现对方不在了,超时重复 1 步骤,重新建立联系。

 

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

 

搭建 UDP 模块

复制内容到剪贴板
  1. public UDPSocket(Context context) {  
  2.         this.mContext = context;  
  3.         int cpuNumbers = Runtime.getRuntime().availableProcessors();  
  4.         // 根据CPU数目初始化线程池  
  5.         mThreadPool = Executors.newFixedThreadPool(cpuNumbers * Config.POOL_SIZE);  
  6.         // 记录创建对象时的时间  
  7.         lastReceiveTime = System.currentTimeMillis();  
  8.         messageReceiveList = new ArrayList<>();  
  9.         Log.d(TAG, "创建 UDP 对象");  
  10. //        createUser();  
  11.     }  

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

复制内容到剪贴板
  1. public void startUDPSocket() {  
  2.         if (client != nullreturn;  
  3.         try {  
  4.             // 表明这个 Socket 在设置的端口上监听数据。  
  5.             client = new DatagramSocket(CLIENT_PORT);  
  6.             client.setReuseAddress(true);  
  7.             if (receivePacket == null) {  
  8.                 // 创建接受数据的 packet  
  9.                 receivePacket = new DatagramPacket(receiveByte, BUFFER_LENGTH);  
  10.             }  
  11.             startSocketThread();  
  12.         } catch (SocketException e) {  
  13.             e.printStackTrace();  
  14.         }  
  15.     }  

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

复制内容到剪贴板
  1. /** 
  2.      * 开启发送数据的线程 
  3.      */  
  4.     private void startSocketThread() {  
  5.         clientThread = new Thread(new Runnable() {  
  6.             @Override  
  7.             public void run() {  
  8.                 receiveMessage();  
  9.             }  
  10.         });  
  11.         isThreadRunning = true;  
  12.         clientThread.start();  
  13.         Log.d(TAG, "开启 UDP 数据接收线程");  
  14.         startHeartbeatTimer();  
  15.     }  

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

复制内容到剪贴板
  1. /** 
  2.      * 处理接受到的消息 
  3.      */  
  4.     private void receiveMessage() {  
  5.         while (isThreadRunning) {  
  6.             try {  
  7.                 if (client != null) {  
  8.                     client.receive(receivePacket);  
  9.                 }  
  10.                 lastReceiveTime = System.currentTimeMillis();  
  11.                 Log.d(TAG, "receive packet success...");  
  12.             } catch (IOException e) {  
  13.                 Log.e(TAG, "UDP数据包接收失败!线程停止");  
  14.                 stopUDPSocket();  
  15.                 e.printStackTrace();  
  16.                 return;  
  17.             }  
  18.             if (receivePacket == null || receivePacket.getLength() == 0) {  
  19.                 Log.e(TAG, "无法接收UDP数据或者接收到的UDP数据为空");  
  20.                 continue;  
  21.             }  
  22.             String strReceive = new String(receivePacket.getData(), receivePacket.getOffset(), receivePacket.getLength());  
  23.             Log.d(TAG, strReceive + " from " + receivePacket.getAddress().getHostAddress() + ":" + receivePacket.getPort());  
  24.             //解析接收到的 json 信息  
  25.             notifyMessageReceive(strReceive);  
  26.             // 每次接收完UDP数据后,重置长度。否则可能会导致下次收到数据包被截断。  
  27.             if (receivePacket != null) {  
  28.                 receivePacket.setLength(BUFFER_LENGTH);  
  29.             }  
  30.         }  
  31.     }  

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

复制内容到剪贴板
  1. /** 
  2.      * 发送心跳包 
  3.      * 
  4.      * @param message 
  5.      */  
  6.     public void sendMessage(final String message) {  
  7.         mThreadPool.execute(new Runnable() {  
  8.             @Override  
  9.             public void run() {  
  10.                 try {  
  11.                     BROADCAST_IP = WifiUtil.getBroadcastAddress();  
  12.                     Log.d(TAG, "BROADCAST_IP:" + BROADCAST_IP);  
  13.                     InetAddress targetAddress = InetAddress.getByName(BROADCAST_IP);  
  14.                     DatagramPacket packet = new DatagramPacket(message.getBytes(), message.length(), targetAddress, CLIENT_PORT);  
  15.                     client.send(packet);  
  16.                     // 数据发送事件  
  17.                     Log.d(TAG, "数据发送成功");  
  18.                 } catch (UnknownHostException e) {  
  19.                     e.printStackTrace();  
  20.                 } catch (IOException e) {  
  21.                     e.printStackTrace();  
  22.                 }  
  23.             }  
  24.         });  
  25.     }  

 

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

 

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

 

搭建 TCP 模块

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

复制内容到剪贴板
  1. /** 
  2.      * 处理 udp 收到的消息 
  3.      * 
  4.      * @param message 
  5.      */  
  6.     private void handleUdpMessage(String message) {  
  7.         try {  
  8.             JSONObject jsonObject = new JSONObject(message);  
  9.             String ip = jsonObject.optString(Config.TCP_IP);  
  10.             String port = jsonObject.optString(Config.TCP_PORT);  
  11.             if (!TextUtils.isEmpty(ip) && !TextUtils.isEmpty(port)) {  
  12.                 startTcpConnection(ip, port);  
  13.             }  
  14.         } catch (JSONException e) {  
  15.             e.printStackTrace();  
  16.         }  
  17.     }  

 

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

怎么获得一个设备的 ip 呢?

复制内容到剪贴板
  1. public String getLocalIPAddress() {  
  2.         WifiInfo wifiInfo = mWifiManager.getConnectionInfo();  
  3.         return intToIp(wifiInfo.getIpAddress());  
  4.     }  
  5.     private static String intToIp(int i) {  
  6.         return (i & 0xFF) + "." + ((i >> 8) & 0xFF) + "." + ((i >> 16) & 0xFF) + "."  
  7.                 + ((i >> 24) & 0xFF);  
  8.     }  

现在拿到了对方的 ip,以及约定好的端口号,终于可以开启一个 TCP 客户端了。

复制内容到剪贴板
  1. private boolean startTcpConnection(final String ip, final int port) {  
  2.         try {  
  3.             if (mSocket == null) {  
  4.                 mSocket = new Socket(ip, port);  
  5.                 mSocket.setKeepAlive(true);  
  6.                 mSocket.setTcpNoDelay(true);  
  7.                 mSocket.setReuseAddress(true);  
  8.             }  
  9.             InputStream is = mSocket.getInputStream();  
  10.             br = new BufferedReader(new InputStreamReader(is));  
  11.             OutputStream os = mSocket.getOutputStream();  
  12.             pw = new PrintWriter(new BufferedWriter(new OutputStreamWriter(os)), true);  
  13.             Log.d(TAG, "tcp 创建成功...");  
  14.             return true;  
  15.         } catch (Exception e) {  
  16.             e.printStackTrace();  
  17.         }  
  18.         return false;  
  19.     }  

 

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

 

细节处理

接下来就是一些细节处理了,比如我们的 UDP 心跳,当 TCP 建立成功之时,我们要停止 UDP 的心跳:

复制内容到剪贴板
  1. if (startTcpConnection(ip, Integer.valueOf(port))) {// 尝试建立 TCP 连接  
  2.                     if (mListener != null) {  
  3.                         mListener.onSuccess();  
  4.                     }  
  5.                     startReceiveTcpThread();  
  6.                     startHeartbeatTimer();  
  7.                 } else {  
  8.                     if (mListener != null) {  
  9.                         mListener.onFailed(Config.ErrorCode.CREATE_TCP_ERROR);  
  10.                     }  
  11.                 }  
  12.             // TCP已经成功建立连接,停止 UDP 的心跳包。  
  13.             public void stopHeartbeatTimer() {  
  14.                 if (timer != null) {  
  15.                     timer.exit();  
  16.                     timer = null;  
  17.                 }  
  18.     }  

对 TCP 连接进行心跳保护:

复制内容到剪贴板
  1. /** 
  2.      * 启动心跳 
  3.      */  
  4.     private void startHeartbeatTimer() {  
  5.         if (timer == null) {  
  6.             timer = new HeartbeatTimer();  
  7.         }  
  8.         timer.setOnScheduleListener(new HeartbeatTimer.OnScheduleListener() {  
  9.             @Override  
  10.             public void onSchedule() {  
  11.                 Log.d(TAG, "timer is onSchedule...");  
  12.                 long duration = System.currentTimeMillis() - lastReceiveTime;  
  13.                 Log.d(TAG, "duration:" + duration);  
  14.                 if (duration > TIME_OUT) {//若超过十五秒都没收到我的心跳包,则认为对方不在线。  
  15.                     Log.d(TAG, "tcp ping 超时,对方已经下线");  
  16.                     stopTcpConnection();  
  17.                     if (mListener != null) {  
  18.                         mListener.onFailed(Config.ErrorCode.PING_TCP_TIMEOUT);  
  19.                     }  
  20.                 } else if (duration > HEARTBEAT_MESSAGE_DURATION) {//若超过两秒他没收到我的心跳包,则重新发一个。  
  21.                     JSONObject jsonObject = new JSONObject();  
  22.                     try {  
  23.                         jsonObject.put(Config.MSG, Config.PING);  
  24.                     } catch (JSONException e) {  
  25.                         e.printStackTrace();  
  26.                     }  
  27.                     sendTcpMessage(jsonObject.toString());  
  28.                 }  
  29.             }  
  30.         });  
  31.         timer.startTimer(01000 * 2);  
  32.     }  

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

复制内容到剪贴板
  1. @Override  
  2.          public void onFailed(int errorCode) {// tcp 异常处理  
  3.              switch (errorCode) {  
  4.                  case Config.ErrorCode.CREATE_TCP_ERROR:  
  5.                      break;  
  6.                  case Config.ErrorCode.PING_TCP_TIMEOUT:  
  7.                      udpSocket.startHeartbeatTimer();  
  8.                      tcpSocket = null;  
  9.                      break;  
  10.              }  
  11.          }  

 

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

 

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

 

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

 

文章信息

发布时间:2018-02-10

发布者:aquwcw

浏览次数: