Android 局域網掃描

本篇簡單介紹經過UDP廣播掃描局域網設備IP,而且經過ZMQ進行通訊。java

UDP鏈接

主要流程:android

1.1 Step1:主機發送廣播信息,並指定接收端的端口(9000) 廣播地址(255.255.255.255)socket

2.2 Step2:主機將數據報以固定報頭而且和用戶數據一塊兒打包封裝在DatagramPacket,爲了防丟失一共發三次,每次發送之後監聽一段時間ide

3.3 Step3:接收端監聽端口(9000),收到數據報之後解析數據若是和和約定的數據格式同樣,則經過數據報獲取主機的ip和端口函數

4.4 Step4:接收端設備經過主機的ip和端口給主機發送響應信息ui

5.5 Step5:主機收到響應信息,主機返回確認響應信息(防止信息丟失)this

6.6 Step6:至少主機和接收端都有了對方的IP地址.net

主機

package com.zjun.searcher;

import android.annotation.TargetApi;
import android.os.Build;

import java.io.IOException;
import java.lang.reflect.Constructor;
import java.lang.reflect.InvocationTargetException;
import java.net.DatagramPacket;
import java.net.DatagramSocket;
import java.net.InetAddress;
import java.net.SocketException;
import java.net.SocketTimeoutException;
import java.net.UnknownHostException;
import java.nio.charset.Charset;
import java.util.HashSet;
import java.util.Set;

public abstract class SearcherHost<T extends SearcherHost.DeviceBean> extends Thread {

    private int mUserDataMaxLen;
    private Class<T> mDeviceClazz;

    // UDP socket鏈接
    private DatagramSocket mHostSocket;
    // UDP socket對應的數據報
    private DatagramPacket mSendPack;

    // 搜索到的設備列表
    private Set<T> mDeviceSet;

    private byte mPackType;

    private String mDeviceIP;

    public SearcherHost() {
        this(0, DeviceBean.class);
    }

    public SearcherHost(int userDataMaxLen, Class clazz) {
        mDeviceClazz = clazz;
        mUserDataMaxLen = userDataMaxLen;
        mDeviceSet = new HashSet<>();

        try {
        
            /***************** Step1 *****************/
            
            // 實例udp socket ip默認爲本機ip,端口爲全部可用端口中隨機端口
            mHostSocket = new DatagramSocket();
            // 設置接收超時時間
            mHostSocket.setSoTimeout(SearcherConst.RECEIVE_TIME_OUT);

            byte[] sendData = new byte[1024];
            InetAddress broadIP = InetAddress.getByName("255.255.255.255");

            // udp socket 數據報 端口號爲 9000
            mSendPack = new DatagramPacket(sendData, sendData.length, broadIP, SearcherConst.DEVICE_FIND_PORT);
        } catch (SocketException | UnknownHostException e) {
            printLog(e.toString());
            if (mHostSocket != null) {
                mHostSocket.close();
            }
        }
    }

    /**
     * 開始搜索
     * @return true-正常啓動,false-已經start()啓動過,沒法再啓動。若要啓動需從新new
     */
    public boolean search() {
        if (this.getState() != State.NEW) {
            return false;
        }

        this.start();
        return true;
    }

    @Override
    public void run() {
        if (mHostSocket == null || mHostSocket.isClosed() || mSendPack == null) {
            return;
        }

        try {
            onSearchStart();

            /***************** Step2 *****************/
            
            // 開始搜索
            for (int i = 0; i < 3; i++) {


                // 打包搜索數據報,數據報類型爲 `搜索請求`
                mPackType = SearcherConst.PACKET_TYPE_FIND_DEVICE_REQ_10;
                mSendPack.setData(packData(i + 1));
                // 發送搜索廣播
                mHostSocket.send(mSendPack);

                // 監聽來信
                byte[] receData = new byte[2 + mUserDataMaxLen];
                DatagramPacket recePack = new DatagramPacket(receData, receData.length);
                try {
                
                    /***************** Step5 *****************/
                    // 最多接收250個,或超時跳出循環
                    int rspCount = SearcherConst.RESPONSE_DEVICE_MAX;
                    while (rspCount-- > 0) {
                        recePack.setData(receData);
                        mHostSocket.receive(recePack);
                        if (recePack.getLength() > 0) {
                            mDeviceIP = recePack.getAddress().getHostAddress();
                            if (parsePack(recePack)) {
                                printLog("a response from:" + mDeviceIP);
                                // 發送一對一的確認信息。使用接收報,由於接收報中有對方的實際IP,發送報時廣播IP

                                // 打包搜索確認數據報,數據報類型爲 `搜索確認`
                                mPackType = SearcherConst.PACKET_TYPE_FIND_DEVICE_CHK_12;
                                recePack.setData(packData(rspCount));

                                mHostSocket.send(recePack);
                            }
                        }
                    }
                } catch (SocketTimeoutException e) {
                }
                printLog(String.format("the %dth search finished", i));

            }
            // finish
            onSearchFinish(mDeviceSet);
        } catch (IOException e) {
            e.printStackTrace();
        } finally {
            if (mHostSocket != null) {
                mHostSocket.close();
            }
        }

    }

    /**
     * 搜索開始時執行
     */
    public abstract void onSearchStart();

    /**
     * 打包搜索時的用戶數據
     * packed the userData by caller when searching
     */
    protected byte[] packUserData_Search() {
        return new byte[0];
    }

    /**
     * 打包確認時的用戶數據
     * packed userData by caller when checking,and override the method when pack
     */
    protected byte[] packUserData_Check() {
        return new byte[0];
    }


    /**
     * 解析數據
     * parse if have userData
     * @param type 數據類型
     * @param device 設備
     * @param userData 數據
     *
     * @return return the result of parse, true if parse success, else false
     */
    public boolean parseUserData(byte type, T device, byte[] userData) {
        return true;
    }

    /**
     * 搜索結束後執行
     * @param deviceSet 搜索到的設備集合
     */
    public abstract void onSearchFinish(Set deviceSet);

    /**
     * 打印日誌
     * 由調用者打印,SE和Android不一樣
     */
    public abstract void printLog(String log);


    /**
     * 解析報文
     * 協議:$ + packType(1) + userData(n)
     *
     *  @param pack 數據報
     */
    @TargetApi(Build.VERSION_CODES.KITKAT)
    private boolean parsePack(DatagramPacket pack) {
        if (pack == null || pack.getAddress() == null) {
            return false;
        }

        String ip = pack.getAddress().getHostAddress();
        int port = pack.getPort();
        for (T d : mDeviceSet) {
            if (d.getIp().equals(ip)) {
                return false;
            }
        }

        // 解析頭部數據
        byte[] data = pack.getData();
        int dataLen = pack.getLength();

        if (dataLen < 2 || data[0] != '$' || data[1] != SearcherConst.PACKET_TYPE_FIND_DEVICE_RSP_11) {
            return false;
        }

        T device = null;

        try {
            Constructor constructor = mDeviceClazz.getDeclaredConstructor(String.class, int.class);
            device = (T) constructor.newInstance(ip, port);
        } catch (NoSuchMethodException | InvocationTargetException | InstantiationException | IllegalAccessException e) {
            e.printStackTrace();
        }

        if (device == null) {
            return false;
        }

        if (mUserDataMaxLen == 0 && dataLen == 2) {
            return mDeviceSet.add(device);
        }

        // 解析用戶數據
        int userDataLen = dataLen - 2;
        byte[] userData = new byte[userDataLen];
        System.arraycopy(data, 2, userData, 0, userDataLen);

        return parseUserData(data[1], device, userData) && mDeviceSet.add(device);
    }

    /**
     * 打包搜索報文
     * 協議:$ + packType(1) + sendSeq(4) [+ deviceIpLen(1) + deviceIp(n<=15)] [+ userData]
     *  packType - 報文類型
     *  sendSeq - 發送序列
     *  deviceIpLen - 設備IP長度
     *  deviceIp - 設備IP,僅在確認時攜帶
     *  userData - 用戶數據
     *
     *  @param seq 發送序列號
     */
    private byte[] packData(int seq) {
        byte[] data = new byte[1024];
        int offset = 0;

        // 打包數據頭部
        data[offset++] = '$';

        data[offset++] = mPackType;

        seq = seq == 3 ? 1 : ++seq; // can't use findSeq++
        data[offset++] = (byte) seq;
        data[offset++] = (byte) (seq >> 8 );
        data[offset++] = (byte) (seq >> 16);
        data[offset++] = (byte) (seq >> 24);

        switch (mPackType) {
            // packType爲 `搜索請求`
            case SearcherConst.PACKET_TYPE_FIND_DEVICE_REQ_10: {

                // 打包userData
                byte[] userData = packUserData_Search();
                if (data.length < offset + userData.length) {
                    byte[] tmp = new byte[offset + userData.length];
                    System.arraycopy(data, 0, tmp, 0, offset);
                    data = tmp;
                }
                System.arraycopy(userData, 0, data, offset, userData.length);
                offset += userData.length;
                break;
            }
            // packType爲 `搜索確認`
            case SearcherConst.PACKET_TYPE_FIND_DEVICE_CHK_12: {
                // deviceIp
                byte[] ips = mDeviceIP.getBytes(Charset.forName("UTF-8"));
                data[offset++] = (byte) ips.length;
                System.arraycopy(ips, 0, data, offset, ips.length);
                offset += ips.length;

                // userData
                byte[] userData = packUserData_Check();
                if (data.length < offset + userData.length) {
                    byte[] tmp = new byte[offset + userData.length];
                    System.arraycopy(data, 0, tmp, 0, offset);
                    data = tmp;
                }
                System.arraycopy(userData, 0, data, offset, userData.length);
                offset += userData.length;
                break;
            }
            default:
        }

        byte[] result = new byte[offset];
        System.arraycopy(data, 0, result, 0, offset);
        return result;
    }


    /**
     * 設備Bean
     * 只要IP同樣,則認爲是同一個設備
     */
    public static class DeviceBean{
        String ip;      // IP地址
        int port;       // 端口

        public DeviceBean(){}

        public DeviceBean(String ip, int port) {
            this.ip = ip;
            this.port = port;
        }

        @Override
        public int hashCode() {
            return ip.hashCode();
        }

        @Override
        public boolean equals(Object o) {
            if (o instanceof DeviceBean) {
                return this.ip.equals(((DeviceBean)o).getIp());
            }
            return super.equals(o);
        }

        public String getIp() {
            return ip;
        }

        public void setIp(String ip) {
            this.ip = ip;
        }

        public int getPort() {
            return port;
        }

        public void setPort(int port) {
            this.port = port;
        }

    }
}

接收端

package com.zjun.searcher;

import java.io.IOException;
import java.net.DatagramPacket;
import java.net.DatagramSocket;
import java.net.InetSocketAddress;
import java.net.SocketException;
import java.net.SocketTimeoutException;
import java.nio.charset.Charset;


public abstract class SearcherDevice extends Thread {

    private int mUserDataMaxLen;

    private volatile boolean mOpenFlag;

    private DatagramSocket mSocket;

    /**
     * 構造函數
     * 不須要用戶數據
     */
    public SearcherDevice() {
        this(0);
    }

    /**
     * 構造函數
     *
     * @param userDataMaxLen 搜索主機發送數據的最大長度
     */
    public SearcherDevice(int userDataMaxLen) {
       this.mUserDataMaxLen = userDataMaxLen;
    }

    /**
     * 打開
     * 便可以上線
     */
    public boolean open() {
        // 線程只能start()一次,重啓必須從新new。所以這裏也只能open()一次
        if (this.getState() != State.NEW) {
            return false;
        }

        mOpenFlag = true;
        this.start();
        return true;
    }

    /**
     * 關閉
     */
    public void close() {
        mOpenFlag = false;
    }

    @Override
    public void run() {
        printLog("設備開啓");
        DatagramPacket recePack = null;
        
        
        /***************** Step3 *****************/
        
        try {
            // 實例接收socket 端口號 9000 和發送端一致
            mSocket = new DatagramSocket(SearcherConst.DEVICE_FIND_PORT);
            // 設置接收超時時間
            mSocket.setSoTimeout(SearcherConst.DEVICE_RECEIVE_DEFAULT_TIME_OUT);
            byte[] buf = new byte[32 + mUserDataMaxLen];
            recePack = new DatagramPacket(buf, buf.length);
        } catch (SocketException e) {
            e.printStackTrace();
        }

        if (mSocket == null || mSocket.isClosed() || recePack == null) {
            return;
        }

        
        /***************** Step4 *****************/
        
        while (mOpenFlag) {
            try {
                // 等待接收主機數據報
                mSocket.receive(recePack);

                // 校驗接收的數據報
                if (verifySearchData(recePack)) {
                    byte[] sendData = packData();

                    // 給主機發送確認報文,等待主機回覆
                    DatagramPacket sendPack = new DatagramPacket(sendData, sendData.length, recePack.getAddress(), recePack.getPort());
                    printLog("接收到請求,給主機回覆信息");
                    mSocket.send(sendPack);
                    printLog("等待主機接收確認");
                    mSocket.setSoTimeout(SearcherConst.RECEIVE_TIME_OUT);
                    try {
                        mSocket.receive(recePack);
                        if (verifyCheckData(recePack)) {
                            printLog("確認成功");
                            onDeviceSearched((InetSocketAddress) recePack.getSocketAddress());
                            mOpenFlag = false;
                            break;
                        }
                    } catch (SocketTimeoutException e) {
                    }
                    mSocket.setSoTimeout(SearcherConst.DEVICE_RECEIVE_DEFAULT_TIME_OUT); // 還原鏈接超時
                }

            } catch (IOException e) {
            }
        }
        mSocket.close();
        printLog("設備關閉或已被找到");
    }

    /**
     * 打包響應報文
     * 協議:$ + packType(1) + userData(n)
     *
     */
    private byte[] packData() {
        byte[] data = new byte[1024];
        int offset = 0;
        data[offset++] = '$';
        data[offset++] = SearcherConst.PACKET_TYPE_FIND_DEVICE_RSP_11;

        // add userData
        byte[] userData = packUserData();
        if (userData.length + offset > data.length) {
            byte[] tmp = new byte[userData.length + offset];
            System.arraycopy(data, 0, tmp, 0, offset);
            data = tmp;
        }
        System.arraycopy(userData, 0, data, offset, userData.length);
        offset += userData.length;

        byte[] retVal = new byte[offset];
        System.arraycopy(data, 0, retVal, 0, offset);

        return retVal;
    }


    /**
     * 校驗搜索數據
     * 協議:$ + packType(1) + sendSeq(4) [+ deviceIpLen(1) + deviceIp(n<=15)] [+ userData]
     *  packType - 報文類型
     *  sendSeq - 發送序列
     *  deviceIpLen - 設備IP長度
     *  deviceIp - 設備IP,僅在確認時攜帶
     *  userData - 用戶數據
     */
    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++] != SearcherConst.PACKET_TYPE_FIND_DEVICE_REQ_10) {
            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 > 3) {
            return false;
        }

        if (mUserDataMaxLen == 0 && offset == data.length) {
            return true;
        }

        // has userData
        byte[] userData = new byte[pack.getLength() - offset];
        System.arraycopy(data, offset, userData, 0, userData.length);
        return parseUserData(data[1], userData);
    }

    /**
     * 校驗確認數據
     * 協議:$ + packType(1) + sendSeq(4) [+ deviceIpLen(1) + deviceIp(n<=15)] [+ userData]
     *  packType - 報文類型
     *  sendSeq - 發送序列
     *  deviceIpLen - 設備IP長度
     *  deviceIp - 設備IP,僅在確認時攜帶
     *  userData - 用戶數據
     */
    private boolean verifyCheckData(DatagramPacket pack) {
        if (pack.getLength() < 6 + 1 +7) {
            return false;
        }

        byte[] data = pack.getData();
        int offset = pack.getOffset();
        int sendSeq;
        if (data[offset++] != '$' || data[offset++] != SearcherConst.PACKET_TYPE_FIND_DEVICE_CHK_12) {
            return false;
        }
        sendSeq = data[offset++] & 0xFF;
        sendSeq |= (data[offset++] << 8 ) & 0xFF;
        sendSeq |= (data[offset++] << 16) & 0xFF00;
        sendSeq |= (data[offset++] << 24) & 0xFF0000;
        if (sendSeq < 1 || sendSeq > SearcherConst.RESPONSE_DEVICE_MAX) {
            return false;
        }

        // ip
        int ipLen = data[offset++];
        if (data.length < offset + ipLen) {
            return false;
        }
        String ip = new String(data, offset, ipLen, Charset.forName("UTF-8"));
        offset += ipLen;
        printLog("Device's ip from host=" + ip);
        if (!isOwnIp(ip)) {
            return false;
        }

        if (mUserDataMaxLen == 0 && offset == data.length) {
            return true;
        }

        // has userData
        byte[] userData = new byte[pack.getLength() - offset];
        System.arraycopy(data, offset, userData, 0, userData.length);
        return parseUserData(data[1], userData);

    }

    /**
     * 打包用戶數據
     * 若是調用者須要,則重寫
     * @return
     */
    protected byte[] packUserData() {
        return new byte[0];
    }

    /**
     * 當設備被發現時執行
     */
    public abstract void onDeviceSearched(InetSocketAddress socketAddr);

    /**
     * 獲取本機在Wifi中的IP
     * 默認都是返回true,若是須要真實驗證,需調用本身重寫本方法
     * @param ip 須要判斷的ip地址
     * @return true-是本機地址
     */
    public boolean isOwnIp(String ip){
        return true;
    }

    /**
     * 解析用戶數據
     * 默認返回true,若是調用者有本身的數據,需重寫
     * @param type 類型,搜索請求or搜索確認
     * @param userData 用戶數據
     * @return 解析結果
     */
    public boolean parseUserData(byte type, byte[] userData) {
        return true;
    }

    /**
     * 打印日誌
     * 由調用者打印,SE和Android不一樣
     */
    public abstract void printLog(String log);

}

添加用戶數據

若是想要添加用戶自定義數據,只須要重寫主機和接收端相應的打包和解析方法。線程

  • 接收端打包用戶數據
/**
             * 響應時的打包數據格式
             * dataType(1) + len(4) + data(n)
             */
             
            @Override
            protected byte[] packUserData() {
                String name = "LED燈";
                String room = "客廳";
                try {
                
                    // nameBytes:{76,69,68,-25,-127,-81}
                    byte[] nameBytes = name.getBytes("UTF-8");
                    
                    // nameBytes:{-27,-82,-94,-27,-114,-123}
                    byte[] roomBytes = room.getBytes("UTF-8");
                    
                    // 用戶數據總大小: dataType(1) + len(4) + data(6) + dataType(1) + len(4) + data(6) = 22
                    byte[] data = new byte[5 + nameBytes.length + 5 + roomBytes.length];
                    int offset = 0;

                    // 用1位存數據類型 DEVICE_TYPE_NAME_21 = 0x21;
                    data[offset++] = DEVICE_TYPE_NAME_21;
                    
                    // 用4位存用戶數據大小
                    data[offset++] = (byte) nameBytes.length;
                    data[offset++] = (byte) (nameBytes.length >> 8);
                    data[offset++] = (byte) (nameBytes.length >> 16);
                    data[offset++] = (byte) (nameBytes.length >> 24);
                    
                    System.arraycopy(nameBytes, 0 , data, offset, nameBytes.length);
                    offset += nameBytes.length;
                    
                    // 用1位存數據類型 DEVICE_TYPE_ROOM_22 = 0x22;
                    data[offset++] = DEVICE_TYPE_ROOM_22;
                    
                    data[offset++] = (byte) roomBytes.length;
                    data[offset++] = (byte) (roomBytes.length >> 8);
                    data[offset++] = (byte) (roomBytes.length >> 16);
                    data[offset++] = (byte) (roomBytes.length >> 24);
                    System.arraycopy(roomBytes, 0 , data, offset, roomBytes.length);

                    return data;
                } catch (UnsupportedEncodingException e) {
                    e.printStackTrace();
                }
                return super.packUserData();
            }
  • 主機解析用戶數據
/**
             * 解析用戶數據
             * dataType(1) + len(4) + data(n)
             * @param type 類型
             * @param device 設備
             * @param userData 數據
             * @return true-解析成功
             */
             
            @Override
            public boolean parseUserData(byte type, MyDevice device, byte[] userData) {
                        
                        // 用戶數據若是小於5,數據錯誤
                        if (userData.length < 5) {
                            return false;
                        }
                        int offset = 0;
                        
                        // 解析用戶數據部分
                        while (offset + 5 < userData.length) {
                            byte dataType = userData[offset++];
                            int len = (userData[offset++] & 0xFF)
                                    | ((userData[offset++] & 0xFF) << 8)
                                    | ((userData[offset++] & 0xFF) << 16)
                                    | ((userData[offset++] & 0xFF) << 24);
                            if (len + offset > userData.length) {
                                return false;
                            }
                            
                            switch (dataType) {
                                //解析`LED燈`
                                case DEVICE_TYPE_NAME_21:
                                    String name = null;
                                    try {
                                        name = new String(userData, offset, len, "UTF-8");
                                    } catch (UnsupportedEncodingException e) {
                                        e.printStackTrace();
                                    }
                                    if (name != null) {
                                        device.setName(name);
                                    }
                                    break;
                                //解析`客廳`    
                                case DEVICE_TYPE_ROOM_22:
                                    String room = null;
                                    try {
                                        room = new String(userData, offset, len, "UTF-8");
                                    } catch (UnsupportedEncodingException e) {
                                        e.printStackTrace();
                                    }
                                    if (room != null) {
                                        device.setRoom(room);
                                    }
                                    break;
                                default:
                            }
            }

備註

  • 2017/06/26 路由器 須要開啓SSID 廣播來支持,UDP 廣播。
相關文章
相關標籤/搜索