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

来源:互联网 发布:金芒网络电玩城 编辑:程序博客网 时间:2024/04/30 12:34

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



1 0
原创粉丝点击