編輯:關於Android編程
接上面的UDP,本篇主要討論如何在局域網中搜索所有的設備,這個需求在物聯網應用的比較多,也比較綜合,特把名字加在標題中了。最後一塊是網絡編程的常見問題。
假設你家裡安裝了智能家居,所有的設備都是通過Wifi連接自己家裡的局域網(至於這些設備沒有界面操作,如何連接wifi?有一個比較流行的牛逼技術,叫SmartConfig)。現在這些設備連入到局域網了,那如何通過Android搜索到這些設備?
模擬主機效果圖:
模擬設備效果圖:
這些設備在局域網內,肯定是通過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的三次握手; 數據安全。加入了協議,提高了數據的安全性。下面是廣播實現的代碼,當然也可以用組播來實現。組播因為組播地址的原因,可以進一步加強安全性,代碼中把廣播的網絡那塊改成組播就好了。
主機——搜索類:
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 SetmDeviceSet; 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 ListmDeviceList; 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(); }
因為用了電腦作為測試設備,包括Java工程和Android模擬器,之前就知道Java工程中要網絡通信要關防火牆,但使用的時候,發現Android模擬器、C工程、和Socket網絡工具都可以通信,就Java工程不行。
嘗試了很多方法找原因,如在命令行執行下面的命令,然而無終而返:
查看局域網中其他運行的電腦:net view 路由追蹤:tracert (ip)最後終於找到解決辦法,在“防火牆”的“允許的應用”中需要設置權限。把“Java(TM) Platform SE binary”勾上,並把後面的“專用”和“公用”網絡都勾選上:
其實問題詳細情況是這樣的:無線Wifi連接的設備不能與無線設備通信(內網IP通信),只能與有線設備通信;而有線設備一切正常。
這問題也很郁悶,查了資料也沒有找到解決辦法。但個人感覺這問題肯定是路由器的問題,因為局域網的控制系統就是路由器。幸運的是,我有兩個一模一樣的路由器,另一個路由器應該的。然後兩台路由器,分別連兩台電腦,通過電腦對路由器界面進行對比(英文是硬傷啊⊙﹏⊙b)。
最後鎖定了這個東西“Wireless Isolation within SSID”,就是連接SSID的設備都獨立,無法進行局域網內通信。曾經手滑了一下,點成Enable。改回Disabled,兄弟間就別分開了:
上面就是需求設計,4個類似的布局控件,每次只能選擇一個,然後得到上面對應的錢數。(上面只是效果圖,實際數據是從服務器獲取,然後付到控件上)看到這種
qq空間現在也可以打賞紅包啦啦!據了解,QQ空間打賞紅包在上個月QQ6.5版中就有了,現在打賞的最高金額是200元!那麼晚qq空間打賞功能是什麼?qq空間打
本文實例講述了使用SAX來解析XML。通常來說在Android裡面可以使用SAX和DOM,DOM需要把整個XML文件讀入內存再解析,比較消耗內存,而SAX是基於事件驅動的
如果APP運行在Android 6.0或以上版本的手機,並且target sdk>=23,那麼在使用一些相對敏感的權限時,需要征求用戶的許可。比如讀寫sdcard,