局域网发现设备代码实现:udp组播

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



评论 8
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值