Android网络编程TCP、UDP(三)——UDP实例:搜索局域网所有的设备
来源:互联网 发布:java web filter 编辑:程序博客网 时间:2024/05/16 08:28
接上面的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的三次握手;
- 数据安全。加入了协议,提高了数据的安全性。
下面是广播实现的代码,当然也可以用组播来实现。组播因为组播地址的原因,可以进一步加强安全性,代码中把广播的网络那块改成组播就好了。(组播参考:Android网络编程TCP、UDP(二))
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,兄弟间就别分开了:
本来一篇想搞定的,结果写了三篇,目录还是按原来一篇的来写,有点儿乱(^__^) ……
网络编程,终于赶在这个周末结束前告一段落。
- Android网络编程TCP、UDP(三)——UDP实例:搜索局域网所有的设备
- Android网络编程TCP、UDP(三)——UDP实例:搜索局域网所有的设备
- Android网络编程TCP、UDP(三)——UDP实例:搜索局域网所有的设备
- TCP/UDP 网络编程实例
- Android网络编程TCP、UDP(一)
- Android网络编程TCP、UDP(二)
- Android网络编程TCP、UDP(一)
- Android网络编程TCP、UDP(二)
- Java—网络编程实现UDP聊天室(局域网)
- Android 网络编程——IP、TCP、UDP
- Day20—网络编程常识、TCP、UDP
- C# 网络编程实例 UDP TCP通信
- android局域网通信(udp,tcp等)
- TCP网络编程&&UDP
- 网络编程 UDP TCP
- 网络编程(TCP/UDP)
- 网络编程-UDP-TCP
- TCP/UDP网络编程
- 【OVS2.5.0源码分析】datapath之action分析(2)
- Revit开发之Element操作
- Error: ' ' is not a valid resource name character
- php实现的支持imagemagick及GD库两种处理的缩略图生成类
- SDWebImage & SVProgressHUD
- Android网络编程TCP、UDP(三)——UDP实例:搜索局域网所有的设备
- AFNetworking
- 实践反馈-findbugs
- LeetCode Increasing Triplet Subsequence
- eclipse debug进去断点是提示是否进入debug视图的设置恢复
- java中 == 与.equals()的比较详解
- SnapKit
- js获取url传递参数,js获取url?号后面的参数window.location
- mysql