Android網路程式設計TCP、UDP(三)——UDP例項:搜尋區域網所有的裝置
接上面的UDP,本篇主要討論如何在區域網中搜索所有的裝置,這個需求在物聯網應用的比較多,也比較綜合,特把名字加在標題中了。最後一塊是網路程式設計的常見問題。
3.6 例項:在區域網內搜尋裝置
假設你家裡安裝了智慧家居,所有的裝置都是通過Wifi連線自己家裡的區域網(至於這些裝置沒有介面操作,如何連線wifi?有一個比較流行的牛逼技術,叫SmartConfig)。現在這些裝置連入到區域網了,那如何通過Android搜尋到這些裝置?
模擬主機效果圖:
模擬裝置效果圖:
3.6.1 原理分析
這些裝置在區域網內,肯定是通過DHCP(Dynamic Host Configuration Protocol,動態主機配置協議)來獲取內網IP的。也就是說每個裝置的IP都不是固定的。而我們主要目的就是要獲取這些裝置的IP地址。
也許你說,把手機設定成一個固定的內網IP,然後讓這些裝置來連線這個固定IP。看上去OK啊,但萬一這個IP被佔用了,怎辦?
每個裝置的IP會變,但通訊埠我們肯定可以固定。這就可以運用上面的UDP廣播(或組播)技術。具體流程:
- 主機(Android手機)傳送廣播資訊,並指定對方接收埠為devicePort;
- 自己的傳送埠為系統分配的hostPort,封裝在DatagramSocket中,開始監聽此埠。防丟失,一共發三次,每次傳送後就監聽一段時間;
- 裝置監聽devicePort埠,收到資訊後。首先解析資料驗證是否是自己人(協議)發過來的,否,扔;是,則通過資料報獲取對方的IP地址與埠hostPort;
- 裝置通過獲取到的IP地址與埠hostPort,給主機發送響應資訊;
- 主機收到裝置的響應,就可以知道裝置的IP地址了。同時主機返回確認資訊給裝置,防止裝置發給主機的響應資訊丟失,畢竟是UDP;
- 有了IP地址,就可以為所欲為了,比如:建立安全連線TCP。
本解決方法有以下特點:
- 靈活性高。主機使用系統自動分配埠,不用擔心埠被其他軟體佔用;
- 搜尋迅速。使用了UDP廣播,所有區域網內的裝置幾乎同時可以接受到資訊;
- 連線安全。為了避免UDP的不安全性,使用了類似TCP的三次握手;
- 資料安全。加入了協議,提高了資料的安全性。
下面是廣播實現的程式碼,當然也可以用組播來實現。組播因為組播地址的原因,可以進一步加強安全性,程式碼中把廣播的網路那塊改成組播就好了。(
3.6.2 程式碼實現
主機——搜尋類:
import android.util.Log;
import java.io.IOException;
import java.net.DatagramPacket;
import java.net.DatagramSocket;
import java.net.InetAddress;
import java.net.SocketTimeoutException;
import java.nio.charset.Charset;
import java.util.HashSet;
import java.util.Set;
/**
* 裝置搜尋類
* Created by zjun on 2016/9/3.
*/
public abstract class DeviceSearcher extends Thread {
private static final String TAG = DeviceSearcher.class.getSimpleName();
private static final int DEVICE_FIND_PORT = 9000;
private static final int RECEIVE_TIME_OUT = 1500; // 接收超時時間
private static final int RESPONSE_DEVICE_MAX = 200; // 響應裝置的最大個數,防止UDP廣播攻擊
private static final byte PACKET_TYPE_FIND_DEVICE_REQ_10 = 0x10; // 搜尋請求
private static final byte PACKET_TYPE_FIND_DEVICE_RSP_11 = 0x11; // 搜尋響應
private static final byte PACKET_TYPE_FIND_DEVICE_CHK_12 = 0x12; // 搜尋確認
private static final byte PACKET_DATA_TYPE_DEVICE_NAME_20 = 0x20;
private static final byte PACKET_DATA_TYPE_DEVICE_ROOM_21 = 0x21;
private DatagramSocket hostSocket;
private Set<DeviceBean> mDeviceSet;
private byte mPackType;
private String mDeviceIP;
DeviceSearcher() {
mDeviceSet = new HashSet<>();
}
@Override
public void run() {
try {
onSearchStart();
hostSocket = new DatagramSocket();
// 設定接收超時時間
hostSocket.setSoTimeout(RECEIVE_TIME_OUT);
byte[] sendData = new byte[1024];
InetAddress broadIP = InetAddress.getByName("255.255.255.255");
DatagramPacket sendPack = new DatagramPacket(sendData, sendData.length, broadIP, DEVICE_FIND_PORT);
for (int i = 0; i < 3; i++) {
// 傳送搜尋廣播
mPackType = PACKET_TYPE_FIND_DEVICE_REQ_10;
sendPack.setData(packData(i + 1));
hostSocket.send(sendPack);
// 監聽來信
byte[] receData = new byte[1024];
DatagramPacket recePack = new DatagramPacket(receData, receData.length);
try {
// 最多接收200個,或超時跳出迴圈
int rspCount = RESPONSE_DEVICE_MAX;
while (rspCount-- > 0) {
recePack.setData(receData);
hostSocket.receive(recePack);
if (recePack.getLength() > 0) {
mDeviceIP = recePack.getAddress().getHostAddress();
if (parsePack(recePack)) {
Log.i(TAG, "@@@zjun: 裝置上線:" + mDeviceIP);
// 傳送一對一的確認資訊。使用接收報,因為接收報中有對方的實際IP,傳送報時廣播IP
mPackType = PACKET_TYPE_FIND_DEVICE_CHK_12;
recePack.setData(packData(rspCount)); // 注意:設定資料的同時,把recePack.getLength()也改變了
hostSocket.send(recePack);
}
}
}
} catch (SocketTimeoutException e) {
}
Log.i(TAG, "@@@zjun: 結束搜尋" + i);
}
onSearchFinish(mDeviceSet);
} catch (IOException e) {
e.printStackTrace();
} finally {
if (hostSocket != null) {
hostSocket.close();
}
}
}
/**
* 搜尋開始時執行
*/
public abstract void onSearchStart();
/**
* 搜尋結束後執行
* @param deviceSet 搜尋到的裝置集合
*/
public abstract void onSearchFinish(Set deviceSet);
/**
* 解析報文
* 協議:$ + packType(1) + data(n)
* data: 由n組資料,每組的組成結構type(1) + length(4) + data(length)
* type型別中包含name、room型別,但name必須在最前面
*/
private boolean parsePack(DatagramPacket pack) {
if (pack == null || pack.getAddress() == null) {
return false;
}
String ip = pack.getAddress().getHostAddress();
int port = pack.getPort();
for (DeviceBean d : mDeviceSet) {
if (d.getIp().equals(ip)) {
return false;
}
}
int dataLen = pack.getLength();
int offset = 0;
byte packType;
byte type;
int len;
DeviceBean device = null;
if (dataLen < 2) {
return false;
}
byte[] data = new byte[dataLen];
System.arraycopy(pack.getData(), pack.getOffset(), data, 0, dataLen);
if (data[offset++] != '$') {
return false;
}
packType = data[offset++];
if (packType != PACKET_TYPE_FIND_DEVICE_RSP_11) {
return false;
}
while (offset + 5 < dataLen) {
type = data[offset++];
len = data[offset++] & 0xFF;
len |= (data[offset++] << 8);
len |= (data[offset++] << 16);
len |= (data[offset++] << 24);
if (offset + len > dataLen) {
break;
}
switch (type) {
case PACKET_DATA_TYPE_DEVICE_NAME_20:
String name = new String(data, offset, len, Charset.forName("UTF-8"));
device = new DeviceBean();
device.setName(name);
device.setIp(ip);
device.setPort(port);
break;
case PACKET_DATA_TYPE_DEVICE_ROOM_21:
String room = new String(data, offset, len, Charset.forName("UTF-8"));
if (device != null) {
device.setRoom(room);
}
break;
default: break;
}
offset += len;
}
if (device != null) {
mDeviceSet.add(device);
return true;
}
return false;
}
/**
* 打包搜尋報文
* 協議:$ + packType(1) + sendSeq(4) + [deviceIP(n<=15)]
* packType - 報文型別
* sendSeq - 傳送序列
* deviceIP - 裝置IP,僅確認時攜帶
*/
private byte[] packData(int seq) {
byte[] data = new byte[1024];
int offset = 0;
data[offset++] = '$';
data[offset++] = mPackType;
seq = seq == 3 ? 1 : ++seq; // can't use findSeq++
data[offset++] = (byte) seq;
data[offset++] = (byte) (seq >> 8 );
data[offset++] = (byte) (seq >> 16);
data[offset++] = (byte) (seq >> 24);
if (mPackType == PACKET_TYPE_FIND_DEVICE_CHK_12) {
byte[] ips = mDeviceIP.getBytes(Charset.forName("UTF-8"));
System.arraycopy(ips, 0, data, offset, ips.length);
offset += ips.length;
}
byte[] result = new byte[offset];
System.arraycopy(data, 0, result, 0, offset);
return result;
}
/**
* 裝置Bean
* 只要IP一樣,則認為是同一個裝置
*/
public static class DeviceBean{
String ip; // IP地址
int port; // 埠
String name; // 裝置名稱
String room; // 裝置所在房間
@Override
public int hashCode() {
return ip.hashCode();
}
@Override
public boolean equals(Object o) {
if (o instanceof DeviceBean) {
return this.ip.equals(((DeviceBean)o).getIp());
}
return super.equals(o);
}
public String getIp() {
return ip;
}
public void setIp(String ip) {
this.ip = ip;
}
public int getPort() {
return port;
}
public void setPort(int port) {
this.port = port;
}
public String getName() {
return name;
}
public void setName(String name) {
this.name = name;
}
public String getRoom() {
return room;
}
public void setRoom(String room) {
this.room = room;
}
}
}
主機——demo核心程式碼:
private List<DeviceSearcher.DeviceBean> mDeviceList;
private void searchDevices_broadcast() {
new DeviceSearcher() {
@Override
public void onSearchStart() {
startSearch(); // 主要用於在UI上展示正在搜尋
}
@Override
public void onSearchFinish(Set deviceSet) {
endSearch(); // 結束UI上的正在搜尋
mDeviceList.clear();
mDeviceList.addAll(deviceSet);
mHandler.sendEmptyMessage(0); // 在UI上更新裝置列表
}
}.start();
}
裝置——裝置等待搜尋類:
import android.content.Context;
import android.net.wifi.WifiInfo;
import android.net.wifi.WifiManager;
import android.util.Log;
import java.io.IOException;
import java.net.DatagramPacket;
import java.net.DatagramSocket;
import java.net.InetSocketAddress;
import java.net.SocketTimeoutException;
import java.nio.charset.Charset;
/**
* 裝置等待搜尋類
* Created by zjun on 2016/9/4.
*/
public abstract class DeviceWaitingSearch extends Thread {
private final String TAG = DeviceWaitingSearch.class.getSimpleName();
private static final int DEVICE_FIND_PORT = 9000;
private static final int RECEIVE_TIME_OUT = 1500; // 接收超時時間,應小於等於主機的超時時間1500
private static final int RESPONSE_DEVICE_MAX = 200; // 響應裝置的最大個數,防止UDP廣播攻擊
private static final byte PACKET_TYPE_FIND_DEVICE_REQ_10 = 0x10; // 搜尋請求
private static final byte PACKET_TYPE_FIND_DEVICE_RSP_11 = 0x11; // 搜尋響應
private static final byte PACKET_TYPE_FIND_DEVICE_CHK_12 = 0x12; // 搜尋確認
private static final byte PACKET_DATA_TYPE_DEVICE_NAME_20 = 0x20;
private static final byte PACKET_DATA_TYPE_DEVICE_ROOM_21 = 0x21;
private Context mContext;
private String deviceName, deviceRoom;
public DeviceWaitingSearch(Context context, String name, String room) {
mContext = context;
deviceName = name;
deviceRoom = room;
}
@Override
public void run() {
DatagramSocket socket = null;
try {
socket = new DatagramSocket(DEVICE_FIND_PORT);
byte[] data = new byte[1024];
DatagramPacket pack = new DatagramPacket(data, data.length);
while (true) {
// 等待主機的搜尋
socket.receive(pack);
if (verifySearchData(pack)) {
byte[] sendData = packData();
DatagramPacket sendPack = new DatagramPacket(sendData, sendData.length, pack.getAddress(), pack.getPort());
Log.i(TAG, "@@@zjun: 給主機回覆資訊");
socket.send(sendPack);
Log.i(TAG, "@@@zjun: 等待主機接收確認");
socket.setSoTimeout(RECEIVE_TIME_OUT);
try {
socket.receive(pack);
if (verifyCheckData(pack)) {
Log.i(TAG, "@@@zjun: 確認成功");
onDeviceSearched((InetSocketAddress) pack.getSocketAddress());
break;
}
} catch (SocketTimeoutException e) {
}
socket.setSoTimeout(0); // 連線超時還原成無窮大,阻塞式接收
}
}
} catch (IOException e) {
e.printStackTrace();
} finally {
if (socket != null) {
socket.close();
}
}
}
/**
* 當裝置被發現時執行
*/
public abstract void onDeviceSearched(InetSocketAddress socketAddr);
/**
* 打包響應報文
* 協議:$ + packType(1) + data(n)
* data: 由n組資料,每組的組成結構type(1) + length(4) + data(length)
* type型別中包含name、room型別,但name必須在最前面
*/
private byte[] packData() {
byte[] data = new byte[1024];
int offset = 0;
data[offset++] = '$';
data[offset++] = PACKET_TYPE_FIND_DEVICE_RSP_11;
byte[] temp = getBytesFromType(PACKET_DATA_TYPE_DEVICE_NAME_20, deviceName);
System.arraycopy(temp, 0, data, offset, temp.length);
offset += temp.length;
temp = getBytesFromType(PACKET_DATA_TYPE_DEVICE_ROOM_21, deviceRoom);
System.arraycopy(temp, 0, data, offset, temp.length);
offset += temp.length;
byte[] retVal = new byte[offset];
System.arraycopy(data, 0, retVal, 0, offset);
return retVal;
}
private byte[] getBytesFromType(byte type, String val) {
byte[] retVal = new byte[0];
if (val != null) {
byte[] valBytes = val.getBytes(Charset.forName("UTF-8"));
retVal = new byte[5 + valBytes.length];
retVal[0] = type;
retVal[1] = (byte) valBytes.length;
retVal[2] = (byte) (valBytes.length >> 8 );
retVal[3] = (byte) (valBytes.length >> 16);
retVal[4] = (byte) (valBytes.length >> 24);
System.arraycopy(valBytes, 0, retVal, 5, valBytes.length);
}
return retVal;
}
/**
* 校驗搜尋資料
* 協議:$ + packType(1) + sendSeq(4)
* packType - 報文型別
* sendSeq - 傳送序列
*/
private boolean verifySearchData(DatagramPacket pack) {
if (pack.getLength() != 6) {
return false;
}
byte[] data = pack.getData();
int offset = pack.getOffset();
int sendSeq;
if (data[offset++] != '$' || data[offset++] != PACKET_TYPE_FIND_DEVICE_REQ_10) {
return false;
}
sendSeq = data[offset++] & 0xFF;
sendSeq |= (data[offset++] << 8 );
sendSeq |= (data[offset++] << 16);
sendSeq |= (data[offset++] << 24);
return sendSeq >= 1 && sendSeq <= 3;
}
/**
* 校驗確認資料
* 協議:$ + packType(1) + sendSeq(4) + deviceIP(n<=15)
* packType - 報文型別
* sendSeq - 傳送序列
* deviceIP - 裝置IP,僅確認時攜帶
*/
private boolean verifyCheckData(DatagramPacket pack) {
if (pack.getLength() < 6) {
return false;
}
byte[] data = pack.getData();
int offset = pack.getOffset();
int sendSeq;
if (data[offset++] != '$' || data[offset++] != PACKET_TYPE_FIND_DEVICE_CHK_12) {
return false;
}
sendSeq = data[offset++] & 0xFF;
sendSeq |= (data[offset++] << 8 );
sendSeq |= (data[offset++] << 16);
sendSeq |= (data[offset++] << 24);
if (sendSeq < 1 || sendSeq > RESPONSE_DEVICE_MAX) {
return false;
}
String ip = new String(data, offset, pack.getLength() - offset, Charset.forName("UTF-8"));
Log.i(TAG, "@@@zjun: ip from host=" + ip);
return ip.equals(getOwnWifiIP());
}
/**
* 獲取本機在Wifi中的IP
*/
private String getOwnWifiIP() {
WifiManager wm = (WifiManager) mContext.getSystemService(Context.WIFI_SERVICE);
if (!wm.isWifiEnabled()) {
return "";
}
// 需加許可權:android.permission.ACCESS_WIFI_STATE
WifiInfo wifiInfo = wm.getConnectionInfo();
int ipInt = wifiInfo.getIpAddress();
String ipAddr = int2Ip(ipInt);
Log.i(TAG, "@@@zjun: 本機IP=" + ipAddr);
return int2Ip(ipInt);
}
/**
* 把int表示的ip轉換成字串ip
*/
private String int2Ip(int i) {
return String.format("%d.%d.%d.%d", i & 0xFF, (i >> 8) & 0xFF, (i >> 16) & 0xFF, (i >> 24) & 0xFF);
}
}
裝置——demo核心程式碼:
private void initData() {
new DeviceWaitingSearch(this, "日燈光", "客廳"){
@Override
public void onDeviceSearched(InetSocketAddress socketAddr) {
pushMsgToMain("已上線,搜尋主機:" + socketAddr.getAddress().getHostAddress() + ":" + socketAddr.getPort());
}
}.start();
}
四、常見問題
4.1 區域網內無法通訊
因為用了電腦作為測試裝置,包括Java工程和Android模擬器,之前就知道Java工程中要網路通訊要關防火牆,但使用的時候,發現Android模擬器、C工程、和Socket網路工具都可以通訊,就Java工程不行。
嘗試了很多方法找原因,如在命令列執行下面的命令,然而無終而返:
- 檢視區域網中其他執行的電腦:net view
- 路由追蹤:tracert (ip)
eg:tracert baidu.com
tracert 192.168.1.10 - 顯示當前TCP/IP網路連線:netstat
最後終於找到解決辦法,在“防火牆”的“允許的應用”中需要設定許可權。把“Java(TM) Platform SE binary”勾上,並把後面的“專用”和“公用”網路都勾選上:
4.2 區域網內只有有線連線的裝置可以通訊,無線裝置卻無法通訊
其實問題詳細情況是這樣的:無線Wifi連線的裝置不能與無線裝置通訊(內網IP通訊),只能與有線裝置通訊;而有線裝置一切正常。
這問題也很鬱悶,查了資料也沒有找到解決辦法。但個人感覺這問題肯定是路由器的問題,因為區域網的控制系統就是路由器。幸運的是,我有兩個一模一樣的路由器,另一個路由器應該的。然後兩臺路由器,分別連兩臺電腦,通過電腦對路由器介面進行對比(英文是硬傷啊⊙﹏⊙b)。
最後鎖定了這個東西“Wireless Isolation within SSID”,就是連線SSID的裝置都獨立,無法進行區域網內通訊。曾經手滑了一下,點成Enable。改回Disabled,兄弟間就別分開了:
本來一篇想搞定的,結果寫了三篇,目錄還是按原來一篇的來寫,有點兒亂(^__^) ……
網路程式設計,終於趕在這個週末結束前告一段落。