解釋
通過UDP廣播查詢服務器的IP地址,然后再建立TCP點對點連接。
應用場景
在服務器IP未知時,並且已知服務器與客戶端明確在一個局域網或者允許組播的子網下。
通過UDP發現服務器地址然后再進行TCP連接。
(PS:萬維網很多路由器對組播進行了限制,所以不能通過UDP報文在萬維網上進行服務器查詢)
主要問題
Android真機和模擬器對組播處理不同
Android不同系統版本對組播處理不同
不同網絡對組播有限制,如部分路由網絡限制UDP報文
簡單實現
傳統組播方式,通過255.255.255.255地址全轉發。
客戶端收到報文后進行相應check,然后通過UDP報文數據獲取服務器的IP地址。
然后通過IP地址連接。
PS:
會有點問題、比如在Android 5.0.1真機(鄙人的手機)上無法接收255.255.255.255組播報文。
也有可能是被路由器過濾掉了,但是在模擬器上一切正常。
后續給出真機上的解決方案
服務器廣播Hello報文代碼:
1 private class BoadrcastDispense implements Runnable { 2 3 @Override 4 public void run() { 5 try { 6 if (UDPSocket == null) { 7 UDPSocket = new DatagramSocket(udpPortServer); 8 } 9 } catch (SocketException e1) { 10 } 11 while (!UDPSocket.isClosed() && flag) { 12 try { 13 Thread.sleep(1000); 14 } catch (InterruptedException e) { 15 e.printStackTrace(); 16 } 17 try { 18 // send message 19 String str = "Hello Client"; 20 byte backData[] = str.getBytes(); 21 DatagramPacket backPacket = new DatagramPacket(backData, 22 backData.length, 23 InetAddress.getByName("255.255.255.255"), 24 udpPortClient); 25 UDPSocket.send(backPacket); 26 } catch (SocketException e) { 27 e.printStackTrace(); 28 } catch (IOException e) { 29 e.printStackTrace(); 30 } 31 } 32 } 33 34 }
客戶端接收Hello報文代碼
1 public static boolean searchServer() { 2 try { 3 // receive packet 4 if (udpSocket == null) { 5 udpSocket = new DatagramSocket(udpPortClient); 6 udpSocket.setSoTimeout(5000); 7 } 8 byte receiveData[] = new byte[bufferSize]; 9 DatagramPacket receivePacket = new DatagramPacket(receiveData, 10 receiveData.length); 11 udpSocket.receive(receivePacket); 12 String recieveStr = new String(receivePacket.getData(), 13 receivePacket.getOffset(), receivePacket.getLength()); 14 System.out.println("服務器IP地址:" 15 + receivePacket.getAddress().getHostAddress()); 16 System.out.println("接收到服務器反饋:" + recieveStr); 17 if (recieveStr.equalsIgnoreCase("Hello Client")) { 18 serverIP = receivePacket.getAddress().getHostAddress(); 19 System.out.println("連接到服務器成功"); 20 return true; 21 } 22 } catch (SocketException e) { 23 e.printStackTrace(); 24 } catch (UnknownHostException e) { 25 e.printStackTrace(); 26 } catch (IOException e) { 27 e.printStackTrace(); 28 } 29 System.out.println("連接到服務器失敗"); 30 return false; 31 32 }
在模擬器上進行調試時,上面代碼就已經足夠了。
但是在真機上運行時,發現一直阻塞在receive那里。
各種 百度 + Google ,依然無果,並且發現很多人都遇到過這個問題。
有三個可能的原因:
-
省電???
Android部分機型為了省電,關閉了wlan的組播功能,但是可以通過代碼開啟。
1 WifiManager manager = (WifiManager) getSystemService(Context.WIFI_SERVICE); 2 WifiManager.MulticastLock lock = manager.createMulticastLock("test wifi"); 3 lock.acquire(); 4 // 要執行的組播操作,如receive等 5 lock.release();
但經過嘗試,並非這個原因導致。
2.路由or網絡設備屏蔽掉了255報文
WIFI網絡屏蔽掉了255.255.255.255的組播報文
但是通過反向發送255報文(Android向PC發,發現PC可以收到)可以成功。
所以排除了這個原因。
PS:
這個原因在其他地方還是很有可能的,很多路由器為了防止廣播風暴,都默認屏蔽了255組播,或者限制了組播數量。
比如華為的設備默認限制為30%,丟包率可想而知
3.子網域無法向外發送廣播
這個就比較理論化了。
說的是家里的路由器的IP地址是192.168.0.1的地址,而192.168.x.x屬於內網域,無法向外廣播啥的....
計算機網絡沒學好,暫時不考慮這個原因
改進實現
由於各種搜索無果,決定換一種思路嘗試,受到在嘗試過程中可以通過Android向PC發送255廣播報文的其他,想到了以下兩種解決方案
1. 利用Android建立服務器,讓PC反向連接到Android
優點是可以簡單快速的建立P2P連接,但是僅限於P2P連接,如果想讓服務器接入多個客戶端,該方法不適用
2. 先通過PC獲取Android的IP地址,然后發定點UDP報文,再從定點UDP報文獲取服務器IP
優點是還是基本的PC做server,android做client的結構,只是要多一步IP中轉
至於我的方法,由於服務器的代碼基本寫完了,不想做大的更改,所以采用的第二種方法,代碼還是比較簡單:
通過Android端向PC端發送255報文
1 public static void sendPacktToServer() { 2 try { 3 // receive packet 4 if (udpSocket == null) { 5 udpSocket = new DatagramSocket(udpPortClient); 6 udpSocket.setSoTimeout(5000); 7 } 8 String str = "Hello Server"; 9 byte[] data = str.getBytes(); 10 DatagramPacket sendPacket = new DatagramPacket(data, 11 data.length, 12 InetAddress.getByName("255.255.255.255"), udpPortServer); 13 lock.acquire(); 14 udpSocket.send(sendPacket); 15 lock.release(); 16 } catch (SocketException e) { 17 e.printStackTrace(); 18 } catch (UnknownHostException e) { 19 e.printStackTrace(); 20 } catch (IOException e) { 21 e.printStackTrace(); 22 } 23 }
PC端接收到Android發送的255報文后直接回饋定向UDP報文,這樣Android端就可以收到該報文了,然后再建立TCP連接
1 private class BoardcastListener implements Runnable { 2 3 @Override 4 public void run() { 5 try { 6 if (UDPSocket == null) { 7 UDPSocket = new DatagramSocket(udpPortServer); 8 } 9 } catch (SocketException e1) { 10 } 11 while (!UDPSocket.isClosed() && flag) { 12 try { 13 // receive message 14 byte[] recvBuf = new byte[bufferSize]; 15 DatagramPacket recvPacket = new DatagramPacket(recvBuf, 16 recvBuf.length); 17 UDPSocket.receive(recvPacket); 18 String recvStr = new String(recvPacket.getData(), 0, 19 recvPacket.getLength()); 20 ServerFrame.getInstance().appendServerStr( 21 "接收到UDP消息: " + recvStr + " 來自:" 22 + recvPacket.getAddress().getHostAddress()); 23 // send back message 24 String str = "Hello Client"; 25 byte backData[] = str.getBytes(); 26 DatagramPacket backPacket = new DatagramPacket(backData, 27 backData.length, recvPacket.getAddress(), 28 udpPortClient); 29 UDPSocket.send(backPacket); 30 } catch (SocketException e) { 31 e.printStackTrace(); 32 } catch (IOException e) { 33 e.printStackTrace(); 34 } 35 } 36 } 37 38 }
至此,基本的核心代碼就全貼出來了,目前經過測試在模擬器上和android真機上均可以連接到服務器
總結
其實主要用途還是局域網內在不知道對方IP情況下怎么建立連接的一種手段,解決方案其實有很多。
比如全通過UDP交互等,但是考慮到TCP不需要手動維護穩定性,所以還是偏好使用TCP連接進行數據傳輸。
(能夠使用場景:局域網游戲、物聯網相互連接啥的)
誠然,也可以手動查詢IP地址,然后輸入IP地址進行TCP連接。
但是在移動設備上大家還是比較喜歡一鍵式的東西,通過UDP進行服務器自動發現,這個功能還是有那么一點點用處。。。。