Android完整Socket解決方案
整體步驟流程
先來說一下整體的步驟思路吧:
發(fā)送 UDP 廣播,大家都知道 UDP 廣播的特性是整個網(wǎng)段的設(shè)備都可以收到這個消息。
接收方收到了 UDP 的廣播,將自己的 ip 地址,和雙方約定的端口號,回復(fù)給 UDP 的發(fā)送方。
發(fā)送方拿到了對方的 ip 地址以及端口號,就可以發(fā)起 TCP 請求了,建立 TCP 連接。
保持一個 TCP 心跳,如果發(fā)現(xiàn)對方不在了,超時重復(fù) 1 步驟,重新建立聯(lián)系。
整體的步驟就和上述的一樣,下面用代碼展開:
搭建 UDP 模塊
public UDPSocket(Context context) { this.mContext = context; int cpuNumbers = Runtime.getRuntime().availableProcessors(); // 根據(jù)CPU數(shù)目初始化線程池 mThreadPool = Executors.newFixedThreadPool(cpuNumbers * Config.POOL_SIZE); // 記錄創(chuàng)建對象時的時間 lastReceiveTime = System.currentTimeMillis(); messageReceiveList = new ArrayList<>(); Log.d(TAG, "創(chuàng)建 UDP 對象"); // createUser(); }
首先進(jìn)行一些初始化操作,準(zhǔn)備線程池,記錄對象初始的時間等等。
public void startUDPSocket() { if (client != null) return; try { // 表明這個 Socket 在設(shè)置的端口上監(jiān)聽數(shù)據(jù)。 client = new DatagramSocket(CLIENT_PORT); client.setReuseAddress(true); if (receivePacket == null) { // 創(chuàng)建接受數(shù)據(jù)的 packet receivePacket = new DatagramPacket(receiveByte, BUFFER_LENGTH); } startSocketThread(); } catch (SocketException e) { e.printStackTrace(); } }
緊接著就創(chuàng)建了真正的一個 UDP Socket 端,DatagramSocket,注意這里傳入的端口號 CLIENT_PORT 的意思是這個 DatagramSocket 在此端口號接收消息。
/** * 開啟發(fā)送數(shù)據(jù)的線程 */ private void startSocketThread() { clientThread = new Thread(new Runnable() { @Override public void run() { receiveMessage(); } }); isThreadRunning = true; clientThread.start(); Log.d(TAG, "開啟 UDP 數(shù)據(jù)接收線程"); startHeartbeatTimer(); }
我們都知道 Socket 中要處理數(shù)據(jù)的發(fā)送和接收,并且發(fā)送和接收都是阻塞的,應(yīng)該放在子線程中,這里就開啟了一個線程,來處理接收到的 UDP 消息(UDP 模塊上一篇文章講得比較詳細(xì)了,所以這里就不詳細(xì)展開了)
/** * 處理接受到的消息 */ 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數(shù)據(jù)包接收失??!線程停止"); stopUDPSocket(); e.printStackTrace(); return; } if (receivePacket == null || receivePacket.getLength() == 0) { Log.e(TAG, "無法接收UDP數(shù)據(jù)或者接收到的UDP數(shù)據(jù)為空"); 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數(shù)據(jù)后,重置長度。否則可能會導(dǎo)致下次收到數(shù)據(jù)包被截斷。 if (receivePacket != null) { receivePacket.setLength(BUFFER_LENGTH); } } }
在子線程接收 UDP 數(shù)據(jù),并且 notifyMessageReceive 方法通過接口來向外通知消息。
/** * 發(fā)送心跳包 * * @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); // 數(shù)據(jù)發(fā)送事件 Log.d(TAG, "數(shù)據(jù)發(fā)送成功"); } catch (UnknownHostException e) { e.printStackTrace(); } catch (IOException e) { e.printStackTrace(); } } }); }
接著 startHeartbeatTimer 開啟一個心跳線程,每間隔五秒,就去廣播一個 UDP 消息。注意這里 getBroadcastAddress 是獲取的網(wǎng)段 ip,發(fā)送這個 UDP 消息的時候,整個網(wǎng)段的所有設(shè)備都可以接收到。
到此為止,我們發(fā)送端的 UDP 算是搭建完成了。
搭建 TCP 模塊
接下來 TCP 模塊該出場了,UDP 發(fā)送心跳廣播的目的就是找到對應(yīng)設(shè)備的 ip 地址和約定好的端口,所以在 UDP 數(shù)據(jù)的接收方法里:
/** * 處理 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 端,發(fā)給我的 UDP 消息,將它的 ip 地址告訴了我,以及我們提前約定好的端口號。
怎么獲得一個設(shè)備的 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); }
現(xiàn)在拿到了對方的 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 創(chuàng)建成功..."); return true; } catch (Exception e) { e.printStackTrace(); } return false; }
當(dāng) TCP 客戶端成功建立的時候,我們就可以通過 TCP Socket 來發(fā)送和接收消息了。
細(xì)節(jié)處理
接下來就是一些細(xì)節(jié)處理了,比如我們的 UDP 心跳,當(dāng) 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已經(jīng)成功建立連接,停止 UDP 的心跳包。 public void stopHeartbeatTimer() { if (timer != null) { timer.exit(); timer = null; } }
對 TCP 連接進(jìn)行心跳保護(hù):
/** * 啟動心跳 */ 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) {//若超過十五秒都沒收到我的心跳包,則認(rèn)為對方不在線。 Log.d(TAG, "tcp ping 超時,對方已經(jīng)下線"); stopTcpConnection(); if (mListener != null) { mListener.onFailed(Config.ErrorCode.PING_TCP_TIMEOUT); } } else if (duration > HEARTBEAT_MESSAGE_DURATION) {//若超過兩秒他沒收到我的心跳包,則重新發(fā)一個。 JSONObject jsonObject = new JSONObject(); try { jsonObject.put(Config.MSG, Config.PING); } catch (JSONException e) { e.printStackTrace(); } sendTcpMessage(jsonObject.toString()); } } }); timer.startTimer(0, 1000 * 2); }
首先會每隔兩秒,就給對方發(fā)送一個 ping 包,看看對面在不在,如果超過 15 秒還沒有回復(fù)我,那就說明對方掉線了,關(guān)閉我這邊的 TCP 端。進(jìn)入 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; } }
當(dāng) TCP 連接超時,我就會重新啟動 UDP 的廣播心跳,尋找等待連接的設(shè)備。進(jìn)入下一個步驟循環(huán)。
對于數(shù)據(jù)傳輸?shù)母袷桨〉鹊燃?xì)節(jié),這個和業(yè)務(wù)相關(guān)。自己來定就好。
還可以根據(jù)自己業(yè)務(wù)的模式,是 CPU 密集型啊,還是 IO 密集型啊,來開啟不同的線程通道。這個就涉及線程的知識了。
源碼分享:https://github.com/itsMelo/AndroidSocket
- 在Android中使用WebSocket實現(xiàn)消息通信的方法詳解
- Android中socket通信的簡單實現(xiàn)
- 使用Android WebSocket實現(xiàn)即時通訊功能
- Android使用MulticastSocket實現(xiàn)多點廣播圖片
- Android通過Socket與服務(wù)器之間進(jìn)行通信的示例
- android使用Socket通信實現(xiàn)多人聊天應(yīng)用
- Android Socket通信實現(xiàn)簡單聊天室
- Android使用Websocket實現(xiàn)聊天室
- android利用websocket協(xié)議與服務(wù)器通信
- 詳解Android 基于TCP和UDP協(xié)議的Socket通信
- Android socket如何實現(xiàn)文件列表動態(tài)訪問
相關(guān)文章
Android開發(fā)之自定義view實現(xiàn)通訊錄列表A~Z字母提示效果【附demo源碼下載】
這篇文章主要介紹了Android開發(fā)之自定義view實現(xiàn)通訊錄列表A~Z字母提示效果,結(jié)合完整實例形式分析了Android獲取通訊錄列表及采用自定義view排列顯示的相關(guān)操作技巧,需要的朋友可以參考下2017-07-07Android安裝apk文件并適配Android 7.0詳解
這篇文章主要介紹了Android安裝apk文件并適配Android 7.0詳解的相關(guān)資料,需要的朋友可以參考下2017-05-05Kotlin?掛起函數(shù)CPS轉(zhuǎn)換原理解析
這篇文章主要為大家介紹了Kotlin?掛起函數(shù)CPS轉(zhuǎn)換原理解析,有需要的朋友可以借鑒參考下,希望能夠有所幫助,祝大家多多進(jìn)步,早日升職加薪2022-12-12