局域网发现设备代码实现:udp组播
2017-02-20 15:57
489 查看
udp 单播、组播、广播都可以实现,单为什么我使用udp组播,
请参考我的上一篇 局域网发现之UDP组播
本篇讲解的是如何使用代码来实现局域网发现功能;
我的需求背景:
使用场景,手机上安装有app A,同一局域网内的电视 上安装有app B,要求当app B 这个版本支持来自A的某个互动功能(比如投屏、游戏控制)时,A就应该能搜到到B所在的设备提示给用户,然后用户才进行互动,局域网搜索设备则是互动的第一步也是前提;
我的设计如下:
在上面的需求中,手机就是这个搜索者,我定义为SearchClient,电视就是这个被搜索者,我定义为SearchServer;
由于我的需求是要用到app级别的发现,并不是设备级别;强调这个是为了和dlna 进行区分,大家如果了解了dlna,发现它的搜索设备是集成到rom 系统中,只要系统支持,那么app便能搜索到设备,但是这样会让dlna使用起来有些局限性,同一设备上app使用dlna必定发现标准都一致,app则不能自定义多种发现条件;比如手机上某个app A,某种场景下下是想要和支持功能A的app互动,另一种场景下需要和支持功能B的app互动,毕竟app之间想要相互发现的条件是根据需求多变的,而且app级别,只要能搜索到,就能确定app已经安装到设备上;我的设计正是适用这种情况;
采用自定义协议:所有协议格式统一采用 prefix + packType(1) + seq(4) +[userData](标志性前缀+消息类型+序列号+自定义数据)
具体的userData 属于集成者自定义部分,主要包括搜索请求数据 和返回的设备数据,格式统一采用:[filedType + filedLength+ filedValue](字段类型标志+字段长度+字段值)
看下面的代码之前,你可能需要补充下基础知识:socket自定义数据格式转化二进制 System.arraycopy方法的使用
下面直接上主要代码:
SearchClient 设备搜索者,要询问的搜索功能在ClientConfig中配置,使用int,int 有32个byte位,这样可以传输最少的数据,表达更多的信息,每个byte来表示一个功能控制位,如果对应的byte功能位和SearchServer支持的功能一致,则
该SearchServer就是要找的目标设备,它就需要在收到搜索请求后做出应答,带上自己的设备信息;
ClientConfig 类实现
SearchClient 类实现
SearchServer 类实现
app如何集成
我提供了一个叫LANDiscoveryLib 的java lib,app 只需引用这个library工程,并进行自己的功能配置,即可。
测试结论:
1.经过许多天的测试,使用udp组播还比较稳定可靠,至少比android自带的nsd发现要靠谱得多;
2. 但是当使用公司大wifi环境测试时,会出现udp丢包导致偶尔搜不到的情况,自己搭建的局域网就基本不存在;
3. 如果是在公司自己搭建局域网,建议调试时,可以让路由器拔掉网线,因为路由器是否联网对于你的调试没有影响,仍然能实现发现功能,因为我有一次让路由器插上公司的网口,大量测试导致了问题,路由器中有DHCP功能,稍有设置或操作不当就会影响公司的内网局域网其他用户。公司不推荐私自使用路由器设备。
4.有个注意的地方:经过测试,我发现udp组播是由当前正在使用的网卡去发送,和设备的网络环境无关,也就是加入的是组播组,并且某些组播还可以夸网段所以可见组播通信是和具体的wifi环境无关;本来我考虑到网络变化时,是否需要先关闭,再重新加入局域网,在新的网络中再次重启发现过程;但是测试发现不需要做这样的处理,
比如在同一局域网环境A,手机能发现电视,其中一个设备切换到网络B,手机无法发现到电视,但是两个都切好到B时,则又能相互发现;意思就是和最初加入组播时,使用的网络无关;
我特意强调这点,是因为我做局域网发现功能也有几个月了,在使用android自带的nsd发现服务器实现时,是和注册的网络环境有关的;由于本篇有些长,这个就不具体解释了;
5. 另外,由于本篇实现的是实时自动发现局域网内的设备,所以会不断循环的去发组播,从4如果只是wifi相互切换是不需要考虑网络变化进行处理的,但更优的做法,如果不是wifi环境,则关闭发现过程,因为这样的组播是么有意义的,具体见demo;
详细的demo 请参见我的源码:https://github.com/amylizxy/udpMulticast
请参考我的上一篇 局域网发现之UDP组播
本篇讲解的是如何使用代码来实现局域网发现功能;
我的需求背景:
使用场景,手机上安装有app A,同一局域网内的电视 上安装有app B,要求当app B 这个版本支持来自A的某个互动功能(比如投屏、游戏控制)时,A就应该能搜到到B所在的设备提示给用户,然后用户才进行互动,局域网搜索设备则是互动的第一步也是前提;
我的设计如下:
在上面的需求中,手机就是这个搜索者,我定义为SearchClient,电视就是这个被搜索者,我定义为SearchServer;
由于我的需求是要用到app级别的发现,并不是设备级别;强调这个是为了和dlna 进行区分,大家如果了解了dlna,发现它的搜索设备是集成到rom 系统中,只要系统支持,那么app便能搜索到设备,但是这样会让dlna使用起来有些局限性,同一设备上app使用dlna必定发现标准都一致,app则不能自定义多种发现条件;比如手机上某个app A,某种场景下下是想要和支持功能A的app互动,另一种场景下需要和支持功能B的app互动,毕竟app之间想要相互发现的条件是根据需求多变的,而且app级别,只要能搜索到,就能确定app已经安装到设备上;我的设计正是适用这种情况;
采用自定义协议:所有协议格式统一采用 prefix + packType(1) + seq(4) +[userData](标志性前缀+消息类型+序列号+自定义数据)
具体的userData 属于集成者自定义部分,主要包括搜索请求数据 和返回的设备数据,格式统一采用:[filedType + filedLength+ filedValue](字段类型标志+字段长度+字段值)
看下面的代码之前,你可能需要补充下基础知识:socket自定义数据格式转化二进制 System.arraycopy方法的使用
下面直接上主要代码:
SearchClient 设备搜索者,要询问的搜索功能在ClientConfig中配置,使用int,int 有32个byte位,这样可以传输最少的数据,表达更多的信息,每个byte来表示一个功能控制位,如果对应的byte功能位和SearchServer支持的功能一致,则
该SearchServer就是要找的目标设备,它就需要在收到搜索请求后做出应答,带上自己的设备信息;
ClientConfig 类实现
package com.example.amyli.my.client; /** * Created by amyli on 2017/2/15. */ public class ClientConfig { private static int askFunc; public static int getAskFunc() { return askFunc; } public static void setAskFunc(int func) { askFunc = func; } }
SearchClient 类实现
package com.example.amyli.my.client; import com.example.amyli.my.base.DeviceData; import com.example.amyli.my.base.RequestSearchData; import com.example.amyli.my.base.SearchConst; import com.example.amyli.my.base.Utils; import com.example.amyli.my.base.BaseUserData; import java.io.IOException; import java.io.UnsupportedEncodingException; import java.net.DatagramPacket; import java.net.InetAddress; import java.net.MulticastSocket; import java.util.HashSet; import java.util.Set; /** * Created by amyli on 2017/2/13. * 局域网中的设备搜索者,包含开启搜索,关闭搜索,以及搜索设备的状态回调 */ public abstract class SearchClient { private int mUserDataMaxLen; private static boolean isOpen = false; private Set<BaseUserData> mDeviceSet; private static MulticastSocket sock; private String mDeviceIP; private DatagramPacket mSendPack; Thread sendThread, receiveThread; private InetAddress multicastInet; private int seq; public SearchClient(int userDataMaxLen) { seq = 0; mUserDataMaxLen = userDataMaxLen; mDeviceSet = new HashSet<>(); try { sock = new MulticastSocket(SearchConst.C_PORT); multicastInet = InetAddress.getByName(SearchConst.MULTICAST_IP); sock.joinGroup(multicastInet); sock.setLoopbackMode(false);// 必须是false才能开启广播功能 byte[] sendData = new byte[1024]; mSendPack = new DatagramPacket(sendData, sendData.length, multicastInet, SearchConst .S_PORT); } catch (IOException e) { printLog(e.toString()); e.printStackTrace(); close(); } } /** * 完成初始化,开始搜索设备 * @return */ public boolean init() { isOpen = true; onSearchStart(); sendThread = new Thread(new Runnable() { @Override public void run() { printLog("start send thread"); send(sock); } }); sendThread.start(); receiveThread = new Thread(new Runnable() { @Override public void run() { printLog("start receive thread"); receive(sock); } }); receiveThread.start(); return true; } /** * 关闭搜索设备,释放资源等 */ public void close() { isOpen = false; if (sendThread != null) { sendThread.interrupt(); } if (receiveThread != null) { receiveThread.interrupt(); } if (sock != null) { try { sock.leaveGroup(multicastInet); } catch (IOException e) { e.printStackTrace(); } finally { sock.close(); } } onSearchFinish(); } /** * 是否开启了局域网搜索功能 * @return */ public static boolean isOpen() { return isOpen; } public static void setIsOpen(boolean isOpen) { SearchClient.isOpen = isOpen; } /** * 开启了搜索功能,回调给app */ public abstract void onSearchStart(); /** * 发现了设备,回调给app * @param dev */ public abstract void onSearchDev(BaseUserData dev); /** * 结束了发现过程,回调给app */ protected abstract void onSearchFinish(); public abstract void printLog(String msg); /** * 发送搜索请求,并能指定想要发现的是支持哪种功能 * @param sock */ private void send(MulticastSocket sock) { if (sock == null || sock.isClosed()) { return; } while (isOpen) { byte mPackType = SearchConst.PACKET_TYPE_FIND_DEVICE_REQ; RequestSearchData request = new RequestSearchData(ClientConfig.getAskFunc()); byte[] userData = RequestSearchData.packRequestUserData (request); if (userData == null) { printLog("userdata null,return"); return; } byte[] bytes = Utils.packData(seq, mPackType, userData); if (bytes == null) { printLog("send null,return"); return; } mSendPack.setData(bytes); try { sock.send(mSendPack); printLog("send seq:" + seq); } catch (IOException e) { e.printStackTrace(); break; } try { Thread.sleep(5000); } catch (InterruptedException e) { e.printStackTrace(); break; } seq++; } close(); } /** * 实现收到server返回设备信息,并解析数据 * @param sock */ private void receive(MulticastSocket sock) { if (sock == null || sock.isClosed()) { return; } byte[] receData = new byte[SearchConst.PACKET_HEADER_LENGTH + mUserDataMaxLen]; DatagramPacket recePack = new DatagramPacket(receData, receData.length); while (isOpen) { recePack.setData(receData); try { sock.receive(recePack); if (recePack.getLength() > 0) { mDeviceIP = recePack.getAddress().getHostAddress(); //check if it's itself //check the ip if already exist if (parseResponsePack(recePack)) { printLog("a response from:" + mDeviceIP); } } } catch (IOException e) { e.printStackTrace(); break; } } close(); } /** * 解析报文 * 协议:$ + packType(1) + userData(n) * * @param pack 数据报 */ private boolean parseResponsePack(DatagramPacket pack) { if (pack == null || pack.getAddress() == null) { return false; } String ip = pack.getAddress().getHostAddress(); int port = pack.getPort(); for (BaseUserData d : mDeviceSet) { if (d.getIp().equals(ip)) { printLog("is the same ip device"); return false; } } // 解析头部数据 byte[] data = pack.getData(); int dataLen = pack.getLength(); int offset = pack.getOffset(); if (dataLen < SearchConst.PACKET_HEADER_LENGTH || data[offset++] != SearchConst .PACKET_PREFIX || data[offset++] != SearchConst.PACKET_TYPE_FIND_DEVICE_RSP) { printLog("parse return false"); return false; } int sendSeq = Utils.bytesToInt(data, offset); printLog("receive response,seq:" + sendSeq); if (sendSeq < 0) { return false; } if (mUserDataMaxLen == 0 && dataLen == SearchConst.PACKET_HEADER_LENGTH) { return false; } // 解析用户数据 int userDataLen = dataLen - SearchConst.PACKET_HEADER_LENGTH; byte[] userData = new byte[userDataLen]; System.arraycopy(data, SearchConst.PACKET_HEADER_LENGTH, userData, 0, userDataLen); DeviceData device = DeviceData.parseDeviceUserData(userData); device.setIp(ip); device.setPort(port); printLog("receive response,device:" + device.toString()); mDeviceSet.add(device); onSearchDev(device); return true; } }
SearchServer 类实现
package com.example.amyli.my.server; /** * Created by amyli on 2017/2/13. */ import com.example.amyli.my.base.BaseUserData; import com.example.amyli.my.base.DeviceData; import com.example.amyli.my.base.RequestSearchData; import com.example.amyli.my.base.SearchConst; import com.example.amyli.my.base.Utils; import java.io.IOException; import java.net.DatagramPacket; import java.net.InetAddress; import java.net.MulticastSocket; public abstract class SearchServer { private int mUserDataMaxLen; private volatile boolean mOpenFlag; private MulticastSocket sock; private InetAddress multicastInet; private Thread serverThread; /** * 构造函数 * 不需要用户数据 */ public SearchServer() { this(0); } /** * 构造函数 * * @param userDataMaxLen 搜索主机发送数据的最大长度 */ public SearchServer(int userDataMaxLen) { this.mUserDataMaxLen = userDataMaxLen; try { sock = new MulticastSocket(SearchConst.S_PORT); multicastInet = InetAddress.getByName(SearchConst.MULTICAST_IP); sock.joinGroup(multicastInet); sock.setLoopbackMode(false);// 必须是false才能开启广播功能 } catch (IOException e) { printLog(e.toString()); e.printStackTrace(); close(); } } /** * 打开 * 即可以上线 */ public boolean init() { printLog("init"); mOpenFlag = true; serverThread = new Thread(new Runnable() { @Override public void run() { receiveAndSend(); } }); serverThread.start(); return true; } /** * 关闭 */ public void close() { printLog("close"); mOpenFlag = false; if (serverThread != null) { serverThread.interrupt(); } if (sock != null) { try { sock.leaveGroup(multicastInet); } catch (IOException e) { e.printStackTrace(); } finally { sock.close(); } } } private int curSeq; public void receiveAndSend() { byte[] buf = new byte[mUserDataMaxLen]; DatagramPacket recePack = new DatagramPacket(buf, buf.length); if (sock == null || sock.isClosed() || recePack == null) { return; } while (mOpenFlag) { try { printLog("server before receive"); // waiting for search from host sock.receive(recePack); // verify the data if (verifySearchReq(recePack)) { byte[] userData = DeviceData.packDeviceData(ServerConfig.getDeviceData()); if (userData == null) { return; } byte[] sendData = Utils.packData(curSeq, SearchConst .PACKET_TYPE_FIND_DEVICE_RSP, userData); if (sendData == null) { return; } printLog("send response,seq:" + curSeq + ",userdata:" + ServerConfig .getDeviceData().toString()); DatagramPacket sendPack = new DatagramPacket(sendData, sendData.length, recePack.getAddress(), recePack.getPort()); sock.send(sendPack); } } catch (IOException e) { printLog(e.toString()); close(); break; } } printLog("设备关闭或已被找到"); } /** * 校验客户端发的搜索请求数据 * 协议:$ + packType(1) + sendSeq(4) [+ deviceIpLen(1) + deviceIp(n<=15)] [+ userData] * packType - 报文类型 * sendSeq - 发送序列 * deviceIpLen - 设备IP长度 * deviceIp - 设备IP,仅在确认时携带 * userData - 用户数据 */ private boolean verifySearchReq(DatagramPacket pack) { if (pack.getLength() < 6) { return false; } byte[] data = pack.getData(); int offset = pack.getOffset(); if (data[offset++] != SearchConst.PACKET_PREFIX || data[offset++] != SearchConst .PACKET_TYPE_FIND_DEVICE_REQ) { printLog("return false"); return false; } int sendSeq = Utils.bytesToInt(data, offset); if (sendSeq < 0) { return false; } offset += SearchConst.INT_LENGTH; printLog("receive seq:" + sendSeq); curSeq = sendSeq; if (mUserDataMaxLen == 0 && offset == data.length) { return true; } // get userData byte[] userData = new byte[pack.getLength() - offset]; System.arraycopy(data, offset, userData, 0, userData.length); RequestSearchData requestSearchData = RequestSearchData.parseRequestUserData(userData); String ip = pack.getAddress().getHostAddress(); int port = pack.getPort(); requestSearchData.setIp(ip); requestSearchData.setPort(port); printLog("receive requestSearchData:" + requestSearchData.toString()); onReceiveSearchReq(requestSearchData); if (requestSearchData.getAskFunc() == ServerConfig.getFunc()) { return true; } return false; } /** * 获取本机在Wifi中的IP * 默认都是返回true,如果需要真实验证,需调用自己重写本方法 * * @param ip 需要判断的ip地址 * @return true-是本机地址 */ public boolean isOwnIp(String ip) { return true; } /** * 打印日志 * 由调用者打印,SE和Android不同 */ public abstract void printLog(String log); public abstract void onReceiveSearchReq(RequestSearchData data); public boolean isOpen() { return mOpenFlag; } }
app如何集成
我提供了一个叫LANDiscoveryLib 的java lib,app 只需引用这个library工程,并进行自己的功能配置,即可。
测试结论:
1.经过许多天的测试,使用udp组播还比较稳定可靠,至少比android自带的nsd发现要靠谱得多;
2. 但是当使用公司大wifi环境测试时,会出现udp丢包导致偶尔搜不到的情况,自己搭建的局域网就基本不存在;
3. 如果是在公司自己搭建局域网,建议调试时,可以让路由器拔掉网线,因为路由器是否联网对于你的调试没有影响,仍然能实现发现功能,因为我有一次让路由器插上公司的网口,大量测试导致了问题,路由器中有DHCP功能,稍有设置或操作不当就会影响公司的内网局域网其他用户。公司不推荐私自使用路由器设备。
4.有个注意的地方:经过测试,我发现udp组播是由当前正在使用的网卡去发送,和设备的网络环境无关,也就是加入的是组播组,并且某些组播还可以夸网段所以可见组播通信是和具体的wifi环境无关;本来我考虑到网络变化时,是否需要先关闭,再重新加入局域网,在新的网络中再次重启发现过程;但是测试发现不需要做这样的处理,
比如在同一局域网环境A,手机能发现电视,其中一个设备切换到网络B,手机无法发现到电视,但是两个都切好到B时,则又能相互发现;意思就是和最初加入组播时,使用的网络无关;
我特意强调这点,是因为我做局域网发现功能也有几个月了,在使用android自带的nsd发现服务器实现时,是和注册的网络环境有关的;由于本篇有些长,这个就不具体解释了;
5. 另外,由于本篇实现的是实时自动发现局域网内的设备,所以会不断循环的去发组播,从4如果只是wifi相互切换是不需要考虑网络变化进行处理的,但更优的做法,如果不是wifi环境,则关闭发现过程,因为这样的组播是么有意义的,具体见demo;
详细的demo 请参见我的源码:https://github.com/amylizxy/udpMulticast
相关文章推荐
- 局域网发现之UDP组播
- java基于socket的组播协议实现代码(局域网聊天室)
- (转)C# 使用UDP组播实现局域网桌面共享
- 转载 CSDN上的 onvif协议的设备发现实现代码 RemoteDiscovery
- 怎么搜索局域网内IPCamera设备的IP、Port等信息,用C#\UDP怎么实现?
- C# 使用UDP组播实现局域网桌面共享
- 【代码练习8】UDP协议实现局域网屏幕广播功能
- 【转】用winpcap实现局域网DNS欺骗之三(代码部分及深入研究)
- 详细分析局域网内通过无线实现Android端与PC端TCP/UDP通信的四种情况
- Netty框架基于UDP实战(一):局域网扫描功能的实现
- VC实现局域网组播 C++交流群QQ群:77278127 非一年以上经验勿入。
- STM32F103ZET6+ENC28J60+LWIP实现UDP组播
- WebSocket学习笔记–IE,IOS,Android等设备的兼容性问题与代码实现
- C#实现UDP打洞 原理及代码(一)
- 批处理之家发现的用批处理实现的系统类应用代码
- Unity中运用UDP广播服务器IP实现局域网联机
- onvif开发之设备发现功能的实现
- 设置Android设备在睡眠期间始终保持WLAN开启的代码实现
- Java UDP 单播、多播(组播)、广播、任播(未实现)
- UDP内网穿透讲解及代码实现、P2P