您的位置:首页 > 编程语言

局域网发现设备代码实现: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 类实现

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
内容来自用户分享和网络整理,不保证内容的准确性,如有侵权内容,可联系管理员处理 点击这里给我发消息