最近在做一个项目,手机app需要发现家庭Wi-Fi下面连接的物联网设备,并获取设备的一些相关信息,思考了几种方案,最终决定使用Udp广播的形式,理由呢,就是Udp使用起来简单,大部分功能Google已经替我们封装好了,直接使用就可以。
很多人说Udp是不可靠的,因为它是一种无连接协议。但是考虑到使用的场景:家庭Wi-Fi,网络环境不会太复杂;每次发送的数据很小等等,我觉得Udp能够满足需求,好了,废话不说,直接上代码。
首先是手机app端,app主动发送Udp广播,并监听指定端口来接收设备单播回来的数据,这里我使用两个线程,一个负责发广播,一个负责接收数据,考虑到可能多个地方会使用到,我决定封装成为一个工具类,关键代码如下:
/**
* 用来发送Udp广播
* @param sendData:需要广播出去的数据
* */
public void send(final UdpScanSendData sendData) {
new Thread(new Runnable() {
@Override
public void run() {
DatagramSocket hostSocket = null;
try {
hostSocket = new DatagramSocket();
Gson gson = new Gson();
//设置30秒超时
hostSocket.setSoTimeout(30000);
//转换为json字符串
String req = gson.toJson(sendData);
Log.e("UDP req", req);
//转换为byte数组
byte[] data = req.getBytes();
//设置广播地址
InetAddress ipBroad = InetAddress.getByName("255.255.255.255");
DatagramPacket packet = new DatagramPacket(data, data.length, ipBroad, 2088);
packet.setData(data);
//发送数据
hostSocket.send(packet);
} catch (Exception e) {
e.printStackTrace();
} finally {
if (hostSocket != null) {
hostSocket.close();
}
}
}
}).start();
}
其中UdpScanSendData 包含了手机端的一些信息,比如手机的Ip地址,监听的端口(这里为2088),以及会话标识,代码如下:
public class UdpScanSendData {
public String IP;
public String port;
public String msgType;
public UdpScanSendData(String IP) {
this.IP = IP;
this.port = 2088;
this.msgType = "SCAN_DEV_REQ";
}
}
这样,我们就通过广播,把手机的信息广播出去,当设备收到广播,解析出手机的Ip,端口号,就可以把设备信息,通过单播的形式发送到手机,因此手机还需要监听指定的端口,来接收数据,代码如下:
/**
* 监听Udp回信
* @param handler 使用handler把接收到的消息传递出去
* @param localIp 手机Ip地址
* */
public void receive(final String localIp, final Handler handler) {
new Thread(new Runnable() {
@Override
public void run() {
if (handler != null) {
handler.sendEmptyMessage(HttpConstance.SCANING);
}
byte[] data = new byte[1024 * 4];
datagramSocket = null;
DatagramPacket dp;
//这里存放接收到的对象
Set<UdpScanReceiveData> set = new HashSet<>();
try {
datagramSocket = new DatagramSocket(null);
datagramSocket.setReuseAddress(true);
//绑定指定端口
datagramSocket.bind(new InetSocketAddress(2017));
datagramSocket.setSoTimeout(30000);
dp = new DatagramPacket(data, data.length);
while (!datagramSocket.isClosed()) {
//接收消息
datagramSocket.receive(dp);
if (dp != null) {
//接收到数据包的ip地址
String devIp = dp.getAddress().getHostAddress();
//过滤本机的Ip地址,由于是发的全网广播,手机也可能会收到
if (!localIp.equals(devIp)) {
//还原出消息字符串
String rsp = new String(dp.getData(), dp.getOffset(), dp.getLength());
Log.e("接收到 ==", rsp);
if (!TextUtils.isEmpty(rsp)) {
//转换为指定的消息对象
UdpScanReceiveData rspData = new Gson().fromJson(rsp, UdpScanReceiveData.class);
if (rspData != null) {
//相关的逻辑处理
set.add(rspData);
if (handler != null) {
handler.obtainMessage(HttpConstance.SCAN_SUCCESS, set).sendToTarget();
}
}
}
}
}
}
} catch (Exception e) {
e.printStackTrace();
} finally {
if (handler != null) {
if (set.size() > 0) {
handler.obtainMessage(HttpConstance.SCAN_COMPLETE, set).sendToTarget();
} else {
handler.obtainMessage(HttpConstance.SCAN_FAILD).sendToTarget();
}
}
if (datagramSocket != null) {
datagramSocket.close();
datagramSocket = null;
}
}
}
}).start();
}
我把这个工具类,定义为单例,把datagramSocket定义为全局变量,这样方便我们在外面关闭upd监听,从上面的代码可以看出,要关闭Udp监听,要么等30秒超时,要么调用datagramSocket.close()这个方法,为此,我们添加一个关闭Udp的方法:
public void close() {
try {
if (datagramSocket != null) {
datagramSocket.close();
}
} catch (Exception e) {
e.printStackTrace();
} finally {
datagramSocket = null;
}
}
这样,手机端的工具类我们就封装好了。到这里,我的工作算是完成了,剩下的就交给终端厂商完成,在手机app上面,点击扫描按钮,就调用工具类的send方法发送一个Udp广播,并监听端口30秒,当设备接收到该广播,就是向手机的ip地址,端口发送一个udp单播,把终端数据发送给手机app。
因为udp的不可靠性,我在activity里面定义了一个子线程,每隔5秒调用一次发送广播的方法,实验表明,发送3次,基本上不会出现扫描不到的情况。
客户端的流程跟手机app刚好相反,开启一个线程监听约定好的端口,这里是2088,当收到广播时候,如果会话标识正确,就把自己的信息广播出去,附上客户端的测试demo,真实设备不是安卓的,该demo仅用于测试
https:///youxibanlv/udpDemo.git