Android教程網
  1. 首頁
  2. Android 技術
  3. Android 手機
  4. Android 系統教程
  5. Android 游戲
 Android教程網 >> Android技術 >> 關於Android編程 >> Android網絡編程TCP、UDP(三)——UDP實例:搜索局域網所有的設備

Android網絡編程TCP、UDP(三)——UDP實例:搜索局域網所有的設備

編輯:關於Android編程


接上面的UDP,本篇主要討論如何在局域網中搜索所有的設備,這個需求在物聯網應用的比較多,也比較綜合,特把名字加在標題中了。最後一塊是網絡編程的常見問題。

3.6 實例:在局域網內搜索設備

假設你家裡安裝了智能家居,所有的設備都是通過Wifi連接自己家裡的局域網(至於這些設備沒有界面操作,如何連接wifi?有一個比較流行的牛逼技術,叫SmartConfig)。現在這些設備連入到局域網了,那如何通過Android搜索到這些設備?

模擬主機效果圖:
這裡寫圖片描述

模擬設備效果圖:
這裡寫圖片描述

3.6.1 原理分析

這些設備在局域網內,肯定是通過DHCP(Dynamic Host Configuration Protocol,動態主機配置協議)來獲取內網IP的。也就是說每個設備的IP都不是固定的。而我們主要目的就是要獲取這些設備的IP地址。

也許你說,把手機設置成一個固定的內網IP,然後讓這些設備來連接這個固定IP。看上去OK啊,但萬一這個IP被占用了,怎辦?

每個設備的IP會變,但通信端口我們肯定可以固定。這就可以運用上面的UDP廣播(或組播)技術。具體流程:

主機(Android手機)發送廣播信息,並指定對方接收端口為devicePort; 自己的發送端口為系統分配的hostPort,封裝在DatagramSocket中,開始監聽此端口。防丟失,一共發三次,每次發送後就監聽一段時間; 設備監聽devicePort端口,收到信息後。首先解析數據驗證是否是自己人(協議)發過來的,否,扔;是,則通過數據報獲取對方的IP地址與端口hostPort; 設備通過獲取到的IP地址與端口hostPort,給主機發送響應信息; 主機收到設備的響應,就可以知道設備的IP地址了。同時主機返回確認信息給設備,防止設備發給主機的響應信息丟失,畢竟是UDP; 有了IP地址,就可以為所欲為了,比如:建立安全連接TCP。

本解決方法有以下特點:

靈活性高。主機使用系統自動分配端口,不用擔心端口被其他軟件占用; 搜索迅速。使用了UDP廣播,所有局域網內的設備幾乎同時可以接受到信息; 連接安全。為了避免UDP的不安全性,使用了類似TCP的三次握手; 數據安全。加入了協議,提高了數據的安全性。

下面是廣播實現的代碼,當然也可以用組播來實現。組播因為組播地址的原因,可以進一步加強安全性,代碼中把廣播的網絡那塊改成組播就好了。

3.6.2 代碼實現

主機——搜索類:

import android.util.Log;
import java.io.IOException;
import java.net.DatagramPacket;
import java.net.DatagramSocket;
import java.net.InetAddress;
import java.net.SocketTimeoutException;
import java.nio.charset.Charset;
import java.util.HashSet;
import java.util.Set;

/**
 * 設備搜索類
 * Created by zjun on 2016/9/3.
 */
public abstract class DeviceSearcher extends Thread {
    private static final String TAG = DeviceSearcher.class.getSimpleName();

    private static final int DEVICE_FIND_PORT = 9000;
    private static final int RECEIVE_TIME_OUT = 1500; // 接收超時時間
    private static final int RESPONSE_DEVICE_MAX = 200; // 響應設備的最大個數,防止UDP廣播攻擊

    private static final byte PACKET_TYPE_FIND_DEVICE_REQ_10 = 0x10; // 搜索請求
    private static final byte PACKET_TYPE_FIND_DEVICE_RSP_11 = 0x11; // 搜索響應
    private static final byte PACKET_TYPE_FIND_DEVICE_CHK_12 = 0x12; // 搜索確認

    private static final byte PACKET_DATA_TYPE_DEVICE_NAME_20 = 0x20;
    private static final byte PACKET_DATA_TYPE_DEVICE_ROOM_21 = 0x21;

    private DatagramSocket hostSocket;
    private Set mDeviceSet;

    private byte mPackType;
    private String mDeviceIP;

    DeviceSearcher() {
        mDeviceSet = new HashSet<>();
    }

    @Override
    public void run() {
        try {
            onSearchStart();
            hostSocket = new DatagramSocket();
            // 設置接收超時時間
            hostSocket.setSoTimeout(RECEIVE_TIME_OUT);

            byte[] sendData = new byte[1024];
            InetAddress broadIP = InetAddress.getByName("255.255.255.255");
            DatagramPacket sendPack = new DatagramPacket(sendData, sendData.length, broadIP, DEVICE_FIND_PORT);

            for (int i = 0; i < 3; i++) {
                // 發送搜索廣播
                mPackType = PACKET_TYPE_FIND_DEVICE_REQ_10;
                sendPack.setData(packData(i + 1));
                hostSocket.send(sendPack);

                // 監聽來信
                byte[] receData = new byte[1024];
                DatagramPacket recePack = new DatagramPacket(receData, receData.length);
                try {
                    // 最多接收200個,或超時跳出循環
                    int rspCount = RESPONSE_DEVICE_MAX;
                    while (rspCount-- > 0) {
                        recePack.setData(receData);
                        hostSocket.receive(recePack);
                        if (recePack.getLength() > 0) {
                            mDeviceIP = recePack.getAddress().getHostAddress();
                            if (parsePack(recePack)) {
                                Log.i(TAG, "@@@zjun: 設備上線:" + mDeviceIP);
                                // 發送一對一的確認信息。使用接收報,因為接收報中有對方的實際IP,發送報時廣播IP
                                mPackType = PACKET_TYPE_FIND_DEVICE_CHK_12;
                                recePack.setData(packData(rspCount)); // 注意:設置數據的同時,把recePack.getLength()也改變了
                                hostSocket.send(recePack);
                            }
                        }
                    }
                } catch (SocketTimeoutException e) {
                }
                Log.i(TAG, "@@@zjun: 結束搜索" + i);
            }
            onSearchFinish(mDeviceSet);
        } catch (IOException e) {
            e.printStackTrace();
        } finally {
            if (hostSocket != null) {
                hostSocket.close();
            }
        }

    }

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

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

    /**
     * 解析報文
     * 協議:$ + packType(1) + data(n)
     *  data: 由n組數據,每組的組成結構type(1) + length(4) + data(length)
     *  type類型中包含name、room類型,但name必須在最前面
     */
    private boolean parsePack(DatagramPacket pack) {
        if (pack == null || pack.getAddress() == null) {
            return false;
        }

        String ip = pack.getAddress().getHostAddress();
        int port = pack.getPort();
        for (DeviceBean d : mDeviceSet) {
            if (d.getIp().equals(ip)) {
                return false;
            }
        }
        int dataLen = pack.getLength();
        int offset = 0;
        byte packType;
        byte type;
        int len;
        DeviceBean device = null;

        if (dataLen < 2) {
            return false;
        }
        byte[] data = new byte[dataLen];
        System.arraycopy(pack.getData(), pack.getOffset(), data, 0, dataLen);

        if (data[offset++] != '$') {
            return false;
        }

        packType = data[offset++];
        if (packType != PACKET_TYPE_FIND_DEVICE_RSP_11) {
            return false;
        }

        while (offset + 5 < dataLen) {
            type = data[offset++];
            len = data[offset++] & 0xFF;
            len |= (data[offset++] << 8);
            len |= (data[offset++] << 16);
            len |= (data[offset++] << 24);

            if (offset + len > dataLen) {
                break;
            }
            switch (type) {
                case PACKET_DATA_TYPE_DEVICE_NAME_20:
                    String name = new String(data, offset, len, Charset.forName("UTF-8"));
                    device = new DeviceBean();
                    device.setName(name);
                    device.setIp(ip);
                    device.setPort(port);
                    break;
                case PACKET_DATA_TYPE_DEVICE_ROOM_21:
                    String room = new String(data, offset, len, Charset.forName("UTF-8"));
                    if (device != null) {
                        device.setRoom(room);
                    }
                    break;
                default: break;
            }
            offset += len;
        }
        if (device != null) {
            mDeviceSet.add(device);
            return true;
        }
        return false;
    }

    /**
     * 打包搜索報文
     * 協議:$ + packType(1) + sendSeq(4) + [deviceIP(n<=15)]
     *  packType - 報文類型
     *  sendSeq - 發送序列
     *  deviceIP - 設備IP,僅確認時攜帶
     */
    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);

        if (mPackType == PACKET_TYPE_FIND_DEVICE_CHK_12) {
            byte[] ips = mDeviceIP.getBytes(Charset.forName("UTF-8"));
            System.arraycopy(ips, 0, data, offset, ips.length);
            offset += ips.length;
        }

        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;       // 端口
        String name;    // 設備名稱
        String room;    // 設備所在房間

        @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;
        }

        public String getName() {
            return name;
        }

        public void setName(String name) {
            this.name = name;
        }

        public String getRoom() {
            return room;
        }

        public void setRoom(String room) {
            this.room = room;
        }
    }
}

主機——demo核心代碼:

private List mDeviceList;
private void searchDevices_broadcast() {
    new DeviceSearcher() {
        @Override
        public void onSearchStart() {
            startSearch(); // 主要用於在UI上展示正在搜索
        }

        @Override
        public void onSearchFinish(Set deviceSet) {
            endSearch(); // 結束UI上的正在搜索
            mDeviceList.clear();
            mDeviceList.addAll(deviceSet);
            mHandler.sendEmptyMessage(0); // 在UI上更新設備列表
        }
    }.start();
}

設備——設備等待搜索類:

import android.content.Context;
import android.net.wifi.WifiInfo;
import android.net.wifi.WifiManager;
import android.util.Log;
import java.io.IOException;
import java.net.DatagramPacket;
import java.net.DatagramSocket;
import java.net.InetSocketAddress;
import java.net.SocketTimeoutException;
import java.nio.charset.Charset;

/**
 * 設備等待搜索類
 * Created by zjun on 2016/9/4.
 */
public abstract class DeviceWaitingSearch extends Thread {
    private final String TAG = DeviceWaitingSearch.class.getSimpleName();

    private static final int DEVICE_FIND_PORT = 9000;
    private static final int RECEIVE_TIME_OUT = 1500; // 接收超時時間,應小於等於主機的超時時間1500
    private static final int RESPONSE_DEVICE_MAX = 200; // 響應設備的最大個數,防止UDP廣播攻擊

    private static final byte PACKET_TYPE_FIND_DEVICE_REQ_10 = 0x10; // 搜索請求
    private static final byte PACKET_TYPE_FIND_DEVICE_RSP_11 = 0x11; // 搜索響應
    private static final byte PACKET_TYPE_FIND_DEVICE_CHK_12 = 0x12; // 搜索確認

    private static final byte PACKET_DATA_TYPE_DEVICE_NAME_20 = 0x20;
    private static final byte PACKET_DATA_TYPE_DEVICE_ROOM_21 = 0x21;

    private Context mContext;
    private String deviceName, deviceRoom;

    public DeviceWaitingSearch(Context context, String name, String room) {
        mContext = context;
        deviceName = name;
        deviceRoom = room;
    }

    @Override
    public void run() {
        DatagramSocket socket = null;
        try {
            socket = new DatagramSocket(DEVICE_FIND_PORT);
            byte[] data = new byte[1024];
            DatagramPacket pack = new DatagramPacket(data, data.length);
            while (true) {
                // 等待主機的搜索
                socket.receive(pack);
                if (verifySearchData(pack)) {
                    byte[] sendData = packData();
                    DatagramPacket sendPack = new DatagramPacket(sendData, sendData.length, pack.getAddress(), pack.getPort());
                    Log.i(TAG, "@@@zjun: 給主機回復信息");
                    socket.send(sendPack);
                    Log.i(TAG, "@@@zjun: 等待主機接收確認");
                    socket.setSoTimeout(RECEIVE_TIME_OUT);
                    try {
                        socket.receive(pack);
                        if (verifyCheckData(pack)) {
                            Log.i(TAG, "@@@zjun: 確認成功");
                            onDeviceSearched((InetSocketAddress) pack.getSocketAddress());
                            break;
                        }
                    } catch (SocketTimeoutException e) {
                    }
                    socket.setSoTimeout(0); // 連接超時還原成無窮大,阻塞式接收
                }
            }
        } catch (IOException e) {
            e.printStackTrace();
        } finally {
            if (socket != null) {
                socket.close();
            }
        }
    }

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

    /**
     * 打包響應報文
     * 協議:$ + packType(1) + data(n)
     *  data: 由n組數據,每組的組成結構type(1) + length(4) + data(length)
     *  type類型中包含name、room類型,但name必須在最前面
     */
    private byte[] packData() {
        byte[] data = new byte[1024];
        int offset = 0;
        data[offset++] = '$';
        data[offset++] = PACKET_TYPE_FIND_DEVICE_RSP_11;

        byte[] temp = getBytesFromType(PACKET_DATA_TYPE_DEVICE_NAME_20, deviceName);
        System.arraycopy(temp, 0, data, offset, temp.length);
        offset += temp.length;

        temp = getBytesFromType(PACKET_DATA_TYPE_DEVICE_ROOM_21, deviceRoom);
        System.arraycopy(temp, 0, data, offset, temp.length);
        offset += temp.length;

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

        return retVal;
    }

    private byte[] getBytesFromType(byte type, String val) {
        byte[] retVal = new byte[0];
        if (val != null) {
            byte[] valBytes = val.getBytes(Charset.forName("UTF-8"));
            retVal = new byte[5 + valBytes.length];
            retVal[0] = type;
            retVal[1] = (byte) valBytes.length;
            retVal[2] = (byte) (valBytes.length >> 8 );
            retVal[3] = (byte) (valBytes.length >> 16);
            retVal[4] = (byte) (valBytes.length >> 24);
            System.arraycopy(valBytes, 0, retVal, 5, valBytes.length);
        }
        return retVal;
    }

    /**
     * 校驗搜索數據
     * 協議:$ + packType(1) + sendSeq(4)
     *  packType - 報文類型
     *  sendSeq - 發送序列
     */
    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++] != PACKET_TYPE_FIND_DEVICE_REQ_10) {
            return false;
        }
        sendSeq = data[offset++] & 0xFF;
        sendSeq |= (data[offset++] << 8 );
        sendSeq |= (data[offset++] << 16);
        sendSeq |= (data[offset++] << 24);
        return sendSeq >= 1 && sendSeq <= 3;
    }

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

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

        String ip = new String(data, offset, pack.getLength() - offset, Charset.forName("UTF-8"));
        Log.i(TAG, "@@@zjun: ip from host=" + ip);
        return ip.equals(getOwnWifiIP());
    }

    /**
     * 獲取本機在Wifi中的IP
     */
    private String getOwnWifiIP() {
        WifiManager wm = (WifiManager) mContext.getSystemService(Context.WIFI_SERVICE);
        if (!wm.isWifiEnabled()) {
            return "";
        }

        // 需加權限:android.permission.ACCESS_WIFI_STATE
        WifiInfo wifiInfo = wm.getConnectionInfo();
        int ipInt = wifiInfo.getIpAddress();
        String ipAddr = int2Ip(ipInt);
        Log.i(TAG, "@@@zjun: 本機IP=" + ipAddr);
        return int2Ip(ipInt);
    }

    /**
     * 把int表示的ip轉換成字符串ip
     */
    private String int2Ip(int i) {
        return String.format("%d.%d.%d.%d", i & 0xFF, (i >> 8) & 0xFF, (i >> 16) & 0xFF, (i >> 24) & 0xFF);
    }
}

設備——demo核心代碼:

private void initData() {
    new DeviceWaitingSearch(this, "日燈光", "客廳"){
        @Override
        public void onDeviceSearched(InetSocketAddress socketAddr) {
            pushMsgToMain("已上線,搜索主機:" + socketAddr.getAddress().getHostAddress() + ":" + socketAddr.getPort());
        }
    }.start();
}

四、常見問題

4.1 局域網內無法通信

因為用了電腦作為測試設備,包括Java工程和Android模擬器,之前就知道Java工程中要網絡通信要關防火牆,但使用的時候,發現Android模擬器、C工程、和Socket網絡工具都可以通信,就Java工程不行。

嘗試了很多方法找原因,如在命令行執行下面的命令,然而無終而返:

查看局域網中其他運行的電腦:net view 路由追蹤:tracert (ip)
eg:tracert baidu.com
tracert 192.168.1.10 顯示當前TCP/IP網絡連接:netstat

最後終於找到解決辦法,在“防火牆”的“允許的應用”中需要設置權限。把“Java(TM) Platform SE binary”勾上,並把後面的“專用”和“公用”網絡都勾選上:
這裡寫圖片描述

4.2 局域網內只有有線連接的設備可以通信,無線設備卻無法通信

其實問題詳細情況是這樣的:無線Wifi連接的設備不能與無線設備通信(內網IP通信),只能與有線設備通信;而有線設備一切正常。

這問題也很郁悶,查了資料也沒有找到解決辦法。但個人感覺這問題肯定是路由器的問題,因為局域網的控制系統就是路由器。幸運的是,我有兩個一模一樣的路由器,另一個路由器應該的。然後兩台路由器,分別連兩台電腦,通過電腦對路由器界面進行對比(英文是硬傷啊⊙﹏⊙b)。

最後鎖定了這個東西“Wireless Isolation within SSID”,就是連接SSID的設備都獨立,無法進行局域網內通信。曾經手滑了一下,點成Enable。改回Disabled,兄弟間就別分開了:
這裡寫圖片描述

  1. 上一頁:
  2. 下一頁:
熱門文章
閱讀排行版
Copyright © Android教程網 All Rights Reserved