本篇簡單介紹經過UDP
廣播掃描局域網設備IP
,而且經過ZMQ
進行通訊。java
主要流程:android
1.1 Step1:主機發送廣播信息,並指定接收端的端口(9000)
廣播地址(255.255.255.255)
socket
2.2 Step2:主機將數據報以固定報頭而且和用戶數據一塊兒打包封裝在DatagramPacket
,爲了防丟失一共發三次,每次發送之後監聽一段時間ide
3.3 Step3:接收端監聽端口(9000)
,收到數據報之後解析數據若是和和約定的數據格式同樣,則經過數據報獲取主機的ip
和端口函數
4.4 Step4:接收端設備經過主機的ip
和端口給主機發送響應信息ui
5.5 Step5:主機收到響應信息,主機返回確認響應信息(防止信息丟失)this
6.6 Step6:至少主機和接收端都有了對方的IP
地址.net
package com.zjun.searcher; import android.annotation.TargetApi; import android.os.Build; import java.io.IOException; import java.lang.reflect.Constructor; import java.lang.reflect.InvocationTargetException; import java.net.DatagramPacket; import java.net.DatagramSocket; import java.net.InetAddress; import java.net.SocketException; import java.net.SocketTimeoutException; import java.net.UnknownHostException; import java.nio.charset.Charset; import java.util.HashSet; import java.util.Set; public abstract class SearcherHost<T extends SearcherHost.DeviceBean> extends Thread { private int mUserDataMaxLen; private Class<T> mDeviceClazz; // UDP socket鏈接 private DatagramSocket mHostSocket; // UDP socket對應的數據報 private DatagramPacket mSendPack; // 搜索到的設備列表 private Set<T> mDeviceSet; private byte mPackType; private String mDeviceIP; public SearcherHost() { this(0, DeviceBean.class); } public SearcherHost(int userDataMaxLen, Class clazz) { mDeviceClazz = clazz; mUserDataMaxLen = userDataMaxLen; mDeviceSet = new HashSet<>(); try { /***************** Step1 *****************/ // 實例udp socket ip默認爲本機ip,端口爲全部可用端口中隨機端口 mHostSocket = new DatagramSocket(); // 設置接收超時時間 mHostSocket.setSoTimeout(SearcherConst.RECEIVE_TIME_OUT); byte[] sendData = new byte[1024]; InetAddress broadIP = InetAddress.getByName("255.255.255.255"); // udp socket 數據報 端口號爲 9000 mSendPack = new DatagramPacket(sendData, sendData.length, broadIP, SearcherConst.DEVICE_FIND_PORT); } catch (SocketException | UnknownHostException e) { printLog(e.toString()); if (mHostSocket != null) { mHostSocket.close(); } } } /** * 開始搜索 * @return true-正常啓動,false-已經start()啓動過,沒法再啓動。若要啓動需從新new */ public boolean search() { if (this.getState() != State.NEW) { return false; } this.start(); return true; } @Override public void run() { if (mHostSocket == null || mHostSocket.isClosed() || mSendPack == null) { return; } try { onSearchStart(); /***************** Step2 *****************/ // 開始搜索 for (int i = 0; i < 3; i++) { // 打包搜索數據報,數據報類型爲 `搜索請求` mPackType = SearcherConst.PACKET_TYPE_FIND_DEVICE_REQ_10; mSendPack.setData(packData(i + 1)); // 發送搜索廣播 mHostSocket.send(mSendPack); // 監聽來信 byte[] receData = new byte[2 + mUserDataMaxLen]; DatagramPacket recePack = new DatagramPacket(receData, receData.length); try { /***************** Step5 *****************/ // 最多接收250個,或超時跳出循環 int rspCount = SearcherConst.RESPONSE_DEVICE_MAX; while (rspCount-- > 0) { recePack.setData(receData); mHostSocket.receive(recePack); if (recePack.getLength() > 0) { mDeviceIP = recePack.getAddress().getHostAddress(); if (parsePack(recePack)) { printLog("a response from:" + mDeviceIP); // 發送一對一的確認信息。使用接收報,由於接收報中有對方的實際IP,發送報時廣播IP // 打包搜索確認數據報,數據報類型爲 `搜索確認` mPackType = SearcherConst.PACKET_TYPE_FIND_DEVICE_CHK_12; recePack.setData(packData(rspCount)); mHostSocket.send(recePack); } } } } catch (SocketTimeoutException e) { } printLog(String.format("the %dth search finished", i)); } // finish onSearchFinish(mDeviceSet); } catch (IOException e) { e.printStackTrace(); } finally { if (mHostSocket != null) { mHostSocket.close(); } } } /** * 搜索開始時執行 */ public abstract void onSearchStart(); /** * 打包搜索時的用戶數據 * packed the userData by caller when searching */ protected byte[] packUserData_Search() { return new byte[0]; } /** * 打包確認時的用戶數據 * packed userData by caller when checking,and override the method when pack */ protected byte[] packUserData_Check() { return new byte[0]; } /** * 解析數據 * parse if have userData * @param type 數據類型 * @param device 設備 * @param userData 數據 * * @return return the result of parse, true if parse success, else false */ public boolean parseUserData(byte type, T device, byte[] userData) { return true; } /** * 搜索結束後執行 * @param deviceSet 搜索到的設備集合 */ public abstract void onSearchFinish(Set deviceSet); /** * 打印日誌 * 由調用者打印,SE和Android不一樣 */ public abstract void printLog(String log); /** * 解析報文 * 協議:$ + packType(1) + userData(n) * * @param pack 數據報 */ @TargetApi(Build.VERSION_CODES.KITKAT) private boolean parsePack(DatagramPacket pack) { if (pack == null || pack.getAddress() == null) { return false; } String ip = pack.getAddress().getHostAddress(); int port = pack.getPort(); for (T d : mDeviceSet) { if (d.getIp().equals(ip)) { return false; } } // 解析頭部數據 byte[] data = pack.getData(); int dataLen = pack.getLength(); if (dataLen < 2 || data[0] != '$' || data[1] != SearcherConst.PACKET_TYPE_FIND_DEVICE_RSP_11) { return false; } T device = null; try { Constructor constructor = mDeviceClazz.getDeclaredConstructor(String.class, int.class); device = (T) constructor.newInstance(ip, port); } catch (NoSuchMethodException | InvocationTargetException | InstantiationException | IllegalAccessException e) { e.printStackTrace(); } if (device == null) { return false; } if (mUserDataMaxLen == 0 && dataLen == 2) { return mDeviceSet.add(device); } // 解析用戶數據 int userDataLen = dataLen - 2; byte[] userData = new byte[userDataLen]; System.arraycopy(data, 2, userData, 0, userDataLen); return parseUserData(data[1], device, userData) && mDeviceSet.add(device); } /** * 打包搜索報文 * 協議:$ + packType(1) + sendSeq(4) [+ deviceIpLen(1) + deviceIp(n<=15)] [+ userData] * packType - 報文類型 * sendSeq - 發送序列 * deviceIpLen - 設備IP長度 * deviceIp - 設備IP,僅在確認時攜帶 * userData - 用戶數據 * * @param seq 發送序列號 */ 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); switch (mPackType) { // packType爲 `搜索請求` case SearcherConst.PACKET_TYPE_FIND_DEVICE_REQ_10: { // 打包userData byte[] userData = packUserData_Search(); if (data.length < offset + userData.length) { byte[] tmp = new byte[offset + userData.length]; System.arraycopy(data, 0, tmp, 0, offset); data = tmp; } System.arraycopy(userData, 0, data, offset, userData.length); offset += userData.length; break; } // packType爲 `搜索確認` case SearcherConst.PACKET_TYPE_FIND_DEVICE_CHK_12: { // deviceIp byte[] ips = mDeviceIP.getBytes(Charset.forName("UTF-8")); data[offset++] = (byte) ips.length; System.arraycopy(ips, 0, data, offset, ips.length); offset += ips.length; // userData byte[] userData = packUserData_Check(); if (data.length < offset + userData.length) { byte[] tmp = new byte[offset + userData.length]; System.arraycopy(data, 0, tmp, 0, offset); data = tmp; } System.arraycopy(userData, 0, data, offset, userData.length); offset += userData.length; break; } default: } 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; // 端口 public DeviceBean(){} public DeviceBean(String ip, int port) { this.ip = ip; this.port = port; } @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; } } }
package com.zjun.searcher; import java.io.IOException; import java.net.DatagramPacket; import java.net.DatagramSocket; import java.net.InetSocketAddress; import java.net.SocketException; import java.net.SocketTimeoutException; import java.nio.charset.Charset; public abstract class SearcherDevice extends Thread { private int mUserDataMaxLen; private volatile boolean mOpenFlag; private DatagramSocket mSocket; /** * 構造函數 * 不須要用戶數據 */ public SearcherDevice() { this(0); } /** * 構造函數 * * @param userDataMaxLen 搜索主機發送數據的最大長度 */ public SearcherDevice(int userDataMaxLen) { this.mUserDataMaxLen = userDataMaxLen; } /** * 打開 * 便可以上線 */ public boolean open() { // 線程只能start()一次,重啓必須從新new。所以這裏也只能open()一次 if (this.getState() != State.NEW) { return false; } mOpenFlag = true; this.start(); return true; } /** * 關閉 */ public void close() { mOpenFlag = false; } @Override public void run() { printLog("設備開啓"); DatagramPacket recePack = null; /***************** Step3 *****************/ try { // 實例接收socket 端口號 9000 和發送端一致 mSocket = new DatagramSocket(SearcherConst.DEVICE_FIND_PORT); // 設置接收超時時間 mSocket.setSoTimeout(SearcherConst.DEVICE_RECEIVE_DEFAULT_TIME_OUT); byte[] buf = new byte[32 + mUserDataMaxLen]; recePack = new DatagramPacket(buf, buf.length); } catch (SocketException e) { e.printStackTrace(); } if (mSocket == null || mSocket.isClosed() || recePack == null) { return; } /***************** Step4 *****************/ while (mOpenFlag) { try { // 等待接收主機數據報 mSocket.receive(recePack); // 校驗接收的數據報 if (verifySearchData(recePack)) { byte[] sendData = packData(); // 給主機發送確認報文,等待主機回覆 DatagramPacket sendPack = new DatagramPacket(sendData, sendData.length, recePack.getAddress(), recePack.getPort()); printLog("接收到請求,給主機回覆信息"); mSocket.send(sendPack); printLog("等待主機接收確認"); mSocket.setSoTimeout(SearcherConst.RECEIVE_TIME_OUT); try { mSocket.receive(recePack); if (verifyCheckData(recePack)) { printLog("確認成功"); onDeviceSearched((InetSocketAddress) recePack.getSocketAddress()); mOpenFlag = false; break; } } catch (SocketTimeoutException e) { } mSocket.setSoTimeout(SearcherConst.DEVICE_RECEIVE_DEFAULT_TIME_OUT); // 還原鏈接超時 } } catch (IOException e) { } } mSocket.close(); printLog("設備關閉或已被找到"); } /** * 打包響應報文 * 協議:$ + packType(1) + userData(n) * */ private byte[] packData() { byte[] data = new byte[1024]; int offset = 0; data[offset++] = '$'; data[offset++] = SearcherConst.PACKET_TYPE_FIND_DEVICE_RSP_11; // add userData byte[] userData = packUserData(); if (userData.length + offset > data.length) { byte[] tmp = new byte[userData.length + offset]; System.arraycopy(data, 0, tmp, 0, offset); data = tmp; } System.arraycopy(userData, 0, data, offset, userData.length); offset += userData.length; byte[] retVal = new byte[offset]; System.arraycopy(data, 0, retVal, 0, offset); return retVal; } /** * 校驗搜索數據 * 協議:$ + packType(1) + sendSeq(4) [+ deviceIpLen(1) + deviceIp(n<=15)] [+ userData] * packType - 報文類型 * sendSeq - 發送序列 * deviceIpLen - 設備IP長度 * deviceIp - 設備IP,僅在確認時攜帶 * userData - 用戶數據 */ 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++] != SearcherConst.PACKET_TYPE_FIND_DEVICE_REQ_10) { return false; } sendSeq = data[offset++] & 0xFF; sendSeq |= (data[offset++] << 8 ) & 0xFF00; sendSeq |= (data[offset++] << 16) & 0xFF0000; sendSeq |= (data[offset++] << 24) & 0xFF000000; if (sendSeq < 1 || sendSeq > 3) { return false; } if (mUserDataMaxLen == 0 && offset == data.length) { return true; } // has userData byte[] userData = new byte[pack.getLength() - offset]; System.arraycopy(data, offset, userData, 0, userData.length); return parseUserData(data[1], userData); } /** * 校驗確認數據 * 協議:$ + packType(1) + sendSeq(4) [+ deviceIpLen(1) + deviceIp(n<=15)] [+ userData] * packType - 報文類型 * sendSeq - 發送序列 * deviceIpLen - 設備IP長度 * deviceIp - 設備IP,僅在確認時攜帶 * userData - 用戶數據 */ private boolean verifyCheckData(DatagramPacket pack) { if (pack.getLength() < 6 + 1 +7) { return false; } byte[] data = pack.getData(); int offset = pack.getOffset(); int sendSeq; if (data[offset++] != '$' || data[offset++] != SearcherConst.PACKET_TYPE_FIND_DEVICE_CHK_12) { return false; } sendSeq = data[offset++] & 0xFF; sendSeq |= (data[offset++] << 8 ) & 0xFF; sendSeq |= (data[offset++] << 16) & 0xFF00; sendSeq |= (data[offset++] << 24) & 0xFF0000; if (sendSeq < 1 || sendSeq > SearcherConst.RESPONSE_DEVICE_MAX) { return false; } // ip int ipLen = data[offset++]; if (data.length < offset + ipLen) { return false; } String ip = new String(data, offset, ipLen, Charset.forName("UTF-8")); offset += ipLen; printLog("Device's ip from host=" + ip); if (!isOwnIp(ip)) { return false; } if (mUserDataMaxLen == 0 && offset == data.length) { return true; } // has userData byte[] userData = new byte[pack.getLength() - offset]; System.arraycopy(data, offset, userData, 0, userData.length); return parseUserData(data[1], userData); } /** * 打包用戶數據 * 若是調用者須要,則重寫 * @return */ protected byte[] packUserData() { return new byte[0]; } /** * 當設備被發現時執行 */ public abstract void onDeviceSearched(InetSocketAddress socketAddr); /** * 獲取本機在Wifi中的IP * 默認都是返回true,若是須要真實驗證,需調用本身重寫本方法 * @param ip 須要判斷的ip地址 * @return true-是本機地址 */ public boolean isOwnIp(String ip){ return true; } /** * 解析用戶數據 * 默認返回true,若是調用者有本身的數據,需重寫 * @param type 類型,搜索請求or搜索確認 * @param userData 用戶數據 * @return 解析結果 */ public boolean parseUserData(byte type, byte[] userData) { return true; } /** * 打印日誌 * 由調用者打印,SE和Android不一樣 */ public abstract void printLog(String log); }
若是想要添加用戶自定義數據,只須要重寫主機和接收端相應的打包和解析方法。線程
/** * 響應時的打包數據格式 * dataType(1) + len(4) + data(n) */ @Override protected byte[] packUserData() { String name = "LED燈"; String room = "客廳"; try { // nameBytes:{76,69,68,-25,-127,-81} byte[] nameBytes = name.getBytes("UTF-8"); // nameBytes:{-27,-82,-94,-27,-114,-123} byte[] roomBytes = room.getBytes("UTF-8"); // 用戶數據總大小: dataType(1) + len(4) + data(6) + dataType(1) + len(4) + data(6) = 22 byte[] data = new byte[5 + nameBytes.length + 5 + roomBytes.length]; int offset = 0; // 用1位存數據類型 DEVICE_TYPE_NAME_21 = 0x21; data[offset++] = DEVICE_TYPE_NAME_21; // 用4位存用戶數據大小 data[offset++] = (byte) nameBytes.length; data[offset++] = (byte) (nameBytes.length >> 8); data[offset++] = (byte) (nameBytes.length >> 16); data[offset++] = (byte) (nameBytes.length >> 24); System.arraycopy(nameBytes, 0 , data, offset, nameBytes.length); offset += nameBytes.length; // 用1位存數據類型 DEVICE_TYPE_ROOM_22 = 0x22; data[offset++] = DEVICE_TYPE_ROOM_22; data[offset++] = (byte) roomBytes.length; data[offset++] = (byte) (roomBytes.length >> 8); data[offset++] = (byte) (roomBytes.length >> 16); data[offset++] = (byte) (roomBytes.length >> 24); System.arraycopy(roomBytes, 0 , data, offset, roomBytes.length); return data; } catch (UnsupportedEncodingException e) { e.printStackTrace(); } return super.packUserData(); }
/** * 解析用戶數據 * dataType(1) + len(4) + data(n) * @param type 類型 * @param device 設備 * @param userData 數據 * @return true-解析成功 */ @Override public boolean parseUserData(byte type, MyDevice device, byte[] userData) { // 用戶數據若是小於5,數據錯誤 if (userData.length < 5) { return false; } int offset = 0; // 解析用戶數據部分 while (offset + 5 < userData.length) { byte dataType = userData[offset++]; int len = (userData[offset++] & 0xFF) | ((userData[offset++] & 0xFF) << 8) | ((userData[offset++] & 0xFF) << 16) | ((userData[offset++] & 0xFF) << 24); if (len + offset > userData.length) { return false; } switch (dataType) { //解析`LED燈` case DEVICE_TYPE_NAME_21: String name = null; try { name = new String(userData, offset, len, "UTF-8"); } catch (UnsupportedEncodingException e) { e.printStackTrace(); } if (name != null) { device.setName(name); } break; //解析`客廳` case DEVICE_TYPE_ROOM_22: String room = null; try { room = new String(userData, offset, len, "UTF-8"); } catch (UnsupportedEncodingException e) { e.printStackTrace(); } if (room != null) { device.setRoom(room); } break; default: } }
2017/06/26
路由器 須要開啓SSID
廣播來支持,UDP
廣播。