/ 今日科技快讯 /
华为 Mate 70 系列手机已于12 月 4 日上午 10:08 正式开售,起售价为 5499 元。本次推出的 Mate 70 系列包括四款机型,分别为 Mate 70、Mate 70 Pro、Mate 70 Pro + 以及 Mate 70 RS 非凡大师版。
/ 作者简介 /
本篇文章转自Wgllss的博客,文章主要分享了局域网下Android设备的数据通信,相信会对大家有所帮助!
原文地址:
https://juejin.cn/post/7444378661934055464
/ 前言 /
前些年,大概2018年时候,开始转入移动嵌入设备开发,当时主要带队公司移动设备组的发展,收到需求:收银机如果在没有网络情况下,下的离线订单存入本地数据库,当有网络情况下,再将本地离线的数据上传到服务器。可能存在极端情况下:如果在无网络情况下下的订单存入本地,在网络正常来临之前,本台设备已经坏了,常规做法:那本地保存的数据永远到不了服务端,这样就造成了数据丢失。怎么做呢?
为了减少此类情况发生,可以在设备没有坏的情况下,将本地保存的未同步上传到服务器的数据,先备份到局域网下其他设备,毕竟设备同时坏的情况下概率大大降低了,这样数据丢失的情况发生的概率也就大大降低了。这里需要解决几个问题?
1. 设备之间怎么通信?
2. Android设备搭建起来服务器供通信,其他设备怎么知道你的服务器地址,怎么知道你的局域网IP地址?
每台设备既是客户端又是服务端 下面我们来看具体实现。
/ 正文 /
客户端通过UDP局域网下Wifi广播去搜索设备
1. 首先配置: 用于设备搜索的端口,设备搜索次数,搜索的最大设备数量,接收超时时间,udp数据包,udp数据包类型:搜索类型,udp数据包类型:搜索应答类型
/**
* 用于设备搜索的端口
*/
public static final int DEVICE_SEARCH_PORT = 8100;
/**
* 设备搜索次数
*/
public static final int SEARCH_DEVICE_TIMES = 3;
/**
* 搜索的最大设备数量
*/
public static final int SEARCH_DEVICE_MAX = 250;
/**
* 接收超时时间
*/
public static final int RECEIVE_TIME_OUT = 1000;
/**
* udp数据包前缀
*/
public static final int PACKET_PREFIX = '$';
/**
* udp数据包类型:搜索类型
*/
public static final int PACKET_TYPE_SEARCH_DEVICE_REQ = 0x10;
/**
* udp数据包类型:搜索应答类型
*/
public static final int PACKET_TYPE_SEARCH_DEVICE_RSP = 0x11;
2. 局域网中设备信息:
public class Device {
//ip地址
private String ip;
//端口号
private int port;
//唯一id
private String uuid;
public Device(String ip, int port, String uuid) {
super();
this.ip = ip;
this.port = port;
this.uuid = uuid;
}
//省略get set 方法...
}
3. 准备广播出去的数据包:sendPacket和接收到广播应答的数据包:recePack,这两个数据包都是以UDP数据包类型。该操作需要在异步线程下执行,避免对UI界面体验造成影响。
@Override
public void run() {
//用于存放已经应答的设备
HashMap devices = new HashMap<>();
try {
if (searchListener != null) {
searchListener.onSearchStart();
}
DatagramSocket socket = new DatagramSocket();
//设置接收等待时长
socket.setSoTimeout(RemoteConst.RECEIVE_TIME_OUT);
byte[] sendData = new byte[1024];
byte[] receData = new byte[1024];
DatagramPacket recePack = new DatagramPacket(receData, receData.length);
//使用广播形式(目标地址设为255.255.255.255)的udp数据包
DatagramPacket sendPacket = new DatagramPacket(sendData, sendData.length, InetAddress.getByName("255.255.255.255"), RemoteConst.DEVICE_SEARCH_PORT);
//搜索指定次数
for (int i = 0; i < RemoteConst.SEARCH_DEVICE_TIMES; i++) {
sendPacket.setData(packSearchData(i + 1));
//发送udp数据包
socket.send(sendPacket);
try {
//限定搜索设备的最大数量
int rspCount = RemoteConst.SEARCH_DEVICE_MAX;
while (rspCount > 0) {
socket.receive(recePack);
final Device device = parseRespData(recePack);
if (device != null) {
if (devices.get(device.getIp()) == null) {
//保存新应答的设备
devices.put(device.getIp(), device);
if (searchListener != null) {
//发现有新的设备
searchListener.onSearchedNewOne(device);
}
}
}
rspCount--;
}
} catch (Exception e) {
// e.printStackTrace();
}
}
socket.close();
} catch (Exception e) {
// e.printStackTrace();
}
if (searchListener != null) {
//广播搜索结束
searchListener.onSearchFinish(devices);
}
}
4. 生成搜索数据包
/**
* 生成搜索数据包
* 格式:$(1) + packType(1) + sendSeq(4) + dataLen(1) + [data]
* packType - 报文类型
* sendSeq - 发送序列
* dataLen - 数据长度
* data - 数据内容
*
* @param seq
* @return
*/
private byte[] packSearchData(int seq) {
byte[] data = new byte[6];
int offset = 0;
data[offset++] = RemoteConst.PACKET_PREFIX;
data[offset++] = RemoteConst.PACKET_TYPE_SEARCH_DEVICE_REQ;
data[offset++] = (byte) seq;
data[offset++] = (byte) (seq >> 8);
data[offset++] = (byte) (seq >> 16);
data[offset++] = (byte) (seq >> 24);
return data;
}
5. 校验和解析应答的数据包,需要排除掉自己设备的IP
/**
* 校验和解析应答的数据包
*
* @param pack udp数据包
* @return
*/
private Device parseRespData(DatagramPacket pack) {
if (pack.getLength() < 2) {
return null;
}
byte[] data = pack.getData();
int offset = pack.getOffset();
//检验数据包格式是否符合要求
if (data[offset++] != RemoteConst.PACKET_PREFIX || data[offset++] != RemoteConst.PACKET_TYPE_SEARCH_DEVICE_RSP) {
return null;
}
int length = data[offset++];
String uuid = new String(data, offset, length);
if (LocalIpAddress.equals(pack.getAddress().getHostAddress())) {
//需要排除掉自己设备的IP
return null;
}
return new Device(pack.getAddress().getHostAddress(), pack.getPort(), uuid);
}
6. 将ip的整数形式转换成ip形式
/**
* 将ip的整数形式转换成ip形式
*
* @param ipInt
* @return
*/
private String int2ip(int ipInt) {
StringBuilder sb = new StringBuilder();
sb.append(ipInt & 0xFF).append(".");
sb.append((ipInt >> 8) & 0xFF).append(".");
sb.append((ipInt >> 16) & 0xFF).append(".");
sb.append((ipInt >> 24) & 0xFF);
return sb.toString();
}
7.获取自己设备IP,用于需要排除掉自己ip设备
/**
* 获取当前ip地址
*
* @param context
* @return
*/
public void setLocalIpAddress(Context context) {
try {
WifiManager wifiManager = (WifiManager) context
.getSystemService(Context.WIFI_SERVICE);
WifiInfo wifiInfo = wifiManager.getConnectionInfo();
int i = wifiInfo.getIpAddress();
LocalIpAddress = int2ip(i);
// return LocalIpAddress;
} catch (Exception ex) {
// return " 获取IP出错鸟!!!!请保证是WIFI,或者请重新打开网络!\n" + ex.getMessage();
}
}
搜索服务端应答实现
1. 接收到搜索包数据后应答处理,需要发送应答数据包 ,发送应答前需要校验收到的是否是搜索包
@Override
public void run() {
try {
//指定接收数据包的端口
socket = new DatagramSocket(RemoteConst.DEVICE_SEARCH_PORT);
byte[] buf = new byte[1024];
DatagramPacket recePacket = new DatagramPacket(buf, buf.length);
openFlag = true;
while (openFlag) {
socket.receive(recePacket);
//校验数据包是否是搜索包
if (verifySearchData(recePacket)) {
//发送搜索应答包
byte[] sendData = packSearchRespData();
DatagramPacket sendPack = new DatagramPacket(sendData, sendData.length, recePacket.getSocketAddress());
if (socket != null) {
socket.send(sendPack);
}
}
}
} catch (Exception e) {
// e.printStackTrace();
// destory();
}
}
2. 生成搜索应答数据的组装,含有:报文类型,发送序列,数据长度,数据内容
/*** 生成搜索应答数据
* 协议:$(1) + packType(1) + sendSeq(4) + dataLen(1) + [data]
* packType - 报文类型
* sendSeq - 发送序列
* dataLen - 数据长度
* data - 数据内容
* @return
*/
private byte[] packSearchRespData() {
byte[] data = new byte[1024];
int offset = 0;
data[offset++] = RemoteConst.PACKET_PREFIX;
data[offset++] = RemoteConst.PACKET_TYPE_SEARCH_DEVICE_RSP;
// 添加UUID数据
byte[] uuid = getUuidData();
data[offset++] = (byte) uuid.length;
System.arraycopy(uuid, 0, data, offset, uuid.length);
offset += uuid.length;
byte[] retVal = new byte[offset];
System.arraycopy(data, 0, retVal, 0, offset);
return retVal;
}
3. 校验搜索数据是否符合协议规范,包括校验:报文类型,发送序列,数据长度,数据内容
/**
* 校验搜索数据是否符合协议规范
* 协议:$(1) + packType(1) + sendSeq(4) + dataLen(1) + [data]
* packType - 报文类型
* sendSeq - 发送序列
* dataLen - 数据长度
* data - 数据内容
*/
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++] != RemoteConst.PACKET_TYPE_SEARCH_DEVICE_REQ) {
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 > RemoteConst.SEARCH_DEVICE_TIMES) {
return false;
}
return true;
}
每台设备搭建服务
1. 借助于该开源库implementation 'org.nanohttpd:nanohttpd:2.3.1' NanoHTTPD 是一个免费、轻量级的 (只有一个 Java 文件) HTTP 服务器,可以很好地嵌入到Java程序中。支持GET, POST, PUT, HEAD 和 DELETE请求,支持文件上传,占用内存很小
2. 接下来就是NanoHTTPD的用法了:基本Get,Post请求如下:
public class AndroidServerImpl extends AndroidServer {
public AndroidServerImpl() {
super();
}
@Override
protected Response servePost(IHTTPSession session) {
String uri = session.getUri();
//可以通过url路径判断哪一个请求
if (uri.startsWith(EnumUrl.backupXXXXX)) {
//开始处理
}
return ResponseExcept(null);
}
@Override
protected Response serveGet(IHTTPSession session) {
String uri = session.getUri();
//可以通过url路径判断哪一个请求
return ResponseExcept(null);
}
3. AndroidServer 端实现:配置服务器端口号
public abstract class AndroidServer extends NanoHTTPD {
protected Gson gson = new Gson();
public AndroidServer(CompositeDisposable compositeDisposable) {
super(EnumUrl.DEFAULT_SERVER_PORT);//配置服务器端口号如 8080,9090等
}
@Override
public Response serve(IHTTPSession session) {
if (session != null) {
Method method = session.getMethod();
if (Method.GET.equals(method)) {
try {
return serveGet(session);
} catch (Exception e) {
return ResponseExcept(e);
}
} else if (Method.POST.equals(method)) {
try {
return servePost(session);
} catch (Exception e) {
return ResponseExcept(e);
}
}
}
return ResponseExcept(null);
}
protected abstract Response servePost(IHTTPSession session);
protected abstract Response serveGet(IHTTPSession session);
protected Response ResponseExcept(Exception e) {
CommonServerResult mCommonServerResult = new CommonServerResult(EnumResult.FAIL, EnumResult.ERROR_MESSAGE_R);
if (e != null) {
mCommonServerResult.setErrorMessage(e.getMessage());
}
return newFixedLengthResponse(Response.Status.NOT_FOUND, NanoHTTPD.MIME_HTML, gson.toJson(mCommonServerResult));
}
}
4. 在android的Service里面可以启动服务,如下
if (!androidServer.isAlive()) {
androidServer.start(60000);
}
5. 在生命周期结束时候停止服务
6. 这样在搜索到局域网内连接的设备后,拿到IP地址,端口号,再通过接口地址,就可以和我们正常请求网络数据一样的操作了。7. 注意:NanoHTTPD是支持文件上传的,也可以直接传输文件,可以简单实现做个类似茄子快传 8. 剩下的就是自己业务逻辑代码实现了。
/ 总结 /
本文重点介绍了,如何实现android 端的本地数据备份功能:
1. 设备之间通信搭建服务器采用NanoHTTPD, 去实现http 请求,Get,Post,及文件传输等
2. 如何通过局域网下广播,发送udp数据包发现对方设备IP地址等
推荐阅读:
我的新书,《第一行代码 第3版》已出版!
原创:写给初学者的Jetpack Compose教程,edge-to-edge全面屏体验
Github Copilot 近期的一次重要更新
欢迎关注我的公众号
学习技术或投稿
长按上图,识别图中二维码即可关注