局域网发现设备代码实现: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



  • 4
    点赞
  • 45
    收藏
    觉得还不错? 一键收藏
  • 8
    评论
### 回答1: UDP组播是一种在网络中传输数据的协议。它可以实现局域网内的IPC设备发现,即在局域网中通过UDP组播的方式来发现IPC设备。以下是实现这一过程的主要步骤。 首先,需要在局域网内配置一个多播组地址。多播组地址是一组预定义的IP地址,用于标识一个多播组。这个地址需要在局域网内唯一,可以通过网络管理员进行配置。 然后,IPC设备需要加入到这个多播组中。这个过程可以通过设备自身的操作或者通过网络管理员的配置来完成。设备加入多播组后,就可以接收到多播组内的消息。 接下来,需要有一个设备或者一个应用程序作为发送方。这个发送方通过UDP协议向多播组地址发送特定的发现消息。这个发现消息可以是一个特定的字符串,用来标识IPC设备。 当IPC设备收到发现消息时,会进行相应的处理。一般来说,设备会根据发现消息中的标识进行识别,如果是自己的标识,就会发送一个应答消息给发送方。 最后,发送方收到设备的应答消息后,就可以确定设备的存在,并进行相应的操作。例如,可以获取设备的IP地址、设备类型等信息,并在应用程序中显示或者进行其他操作。 总的来说,通过UDP组播可以实现局域网内的IPC设备发现。通过加入到预定义的多播组中,IPC设备可以接收到发现消息,在收到消息后发送应答消息,从而完成设备发现。这种方式简化了设备发现的过程,提高了设备的可管理性和易用性。 ### 回答2: UDP组播是一种在局域网实现IPC设备发现的通信方式。IPC设备是指网络摄像头、门禁系统等安防设备组播是一种一对多的通信方式,即一个消息可以同时发送给多个接收者。 要实现局域网IPC设备发现,首先需要选择一个组播地址。组播地址是一个特殊的IP地址,用于向特定的组播组发送消息。然后,在局域网中的每个IPC设备上,都需要开启组播服务,并加入到指定的组播组中。 当一个设备想要发现其他IPC设备时,它会发送一个组播消息到组播地址。其他设备收到这个消息后,会回复一个确认消息。通过这种方式,可以实现设备之间的发现和通信。 为了确保消息的可靠传输,可以使用UDP协议来发送组播消息。UDP协议是一种简单的传输层协议,具有高效和快速的特点。通过UDP组播,可以减少通信的延迟时间和网络带宽的占用。 通过UDP组播实现局域网IPC设备发现,可以方便地管理和控制安防设备。管理员可以通过组播消息了解设备的存在和状态,并进行相应的配置调整。同时,设备之间也可以通过组播消息进行数据同步和共享。 总之,UDP组播是一种实现局域网IPC设备发现的有效通信方式。它可以帮助管理员轻松管理安防设备,并提升设备之间的协作效率。 ### 回答3: 在局域网中,为了实现IPC设备发现和管理,可以使用UDP组播技术。UDP组播是一种将单个数据包发送给多个目标地址的通信方式。 首先,设定一个固定的组播IP地址和端口号,用于IPC设备发现。在局域网中的每个IPC设备都会监听这个组播地址和端口号。 当一个控制中心希望发现局域网中的IPC设备,它会向组播IP地址和端口号发送一个广播消息。这个消息包含了控制中心的信息,如IP地址、端口号等。 每个IPC设备都会接收到这个广播消息并解析其中的内容。如果IPC设备符合控制中心的要求,它会发送一个响应消息给控制中心,包含了自己的信息,如IP地址、端口号、设备型号等。 控制中心接收到响应消息后,可以根据IPC设备的信息进行设备的管理和控制。 UDP组播能够实现局域网中IPC设备发现,主要有以下优势: 1. 通过单次广播消息,可以同时发现多个IPC设备,提高设备发现的效率。 2. 组播方式使用UDP协议,具有较低的网络负载和资源消耗,适用于局域网中的设备发现。 3. 消息通讯采用无连接的方式,简化了设备的配置和管理过程。 通过UDP组播实现局域网IPC设备发现,能够方便、高效地管理和控制局域网中的设备,提升了设备管理的便利性和操作效率。

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论 8
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值