专栏名称: 郭霖
Android技术分享平台,每天都有优质技术文章推送。你还可以向公众号投稿,将自己总结的技术心得分享给大家。
目录
相关文章推荐
郭霖  ·  一篇文章带你彻底掌握Optional ·  昨天  
郭霖  ·  音视频基础能力之 Android ... ·  2 天前  
鸿洋  ·  掌握这17张图,掌握RecyclerView ... ·  2 天前  
51好读  ›  专栏  ›  郭霖

多台Android设备局域网下的数据备份如何实现?

郭霖  · 公众号  · android  · 2024-12-05 08:00

正文



/   今日科技快讯   /


华为 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. 在生命周期结束时候停止服务


androidServer.stop();

6. 这样在搜索到局域网内连接的设备后,拿到IP地址,端口号,再通过接口地址,就可以和我们正常请求网络数据一样的操作了。7. 注意:NanoHTTPD是支持文件上传的,也可以直接传输文件,可以简单实现做个类似茄子快传 8. 剩下的就是自己业务逻辑代码实现了。


/   总结   /


本文重点介绍了,如何实现android 端的本地数据备份功能:


1. 设备之间通信搭建服务器采用NanoHTTPD, 去实现http 请求,Get,Post,及文件传输等


2. 如何通过局域网下广播,发送udp数据包发现对方设备IP地址等


推荐阅读:

我的新书,《第一行代码 第3版》已出版!

原创:写给初学者的Jetpack Compose教程,edge-to-edge全面屏体验

Github Copilot 近期的一次重要更新


欢迎关注我的公众号

学习技术或投稿


长按上图,识别图中二维码即可关注