Android网络编程TCP、UDP(三)——UDP实例:搜索局域网所有的设备


接上面的UDP,本篇主要讨论如何在局域网中搜索所有的设备,这个需求在物联网应用的比较多,也比较综合,特把名字加在标题中了。最后一块是网络编程的常见问题。

3.6 实例:在局域网内搜索设备

假设你家里安装了智能家居,所有的设备都是通过Wifi连接自己家里的局域网(至于这些设备没有界面操作,如何连接wifi?有一个比较流行的牛逼技术,叫SmartConfig)。现在这些设备连入到局域网了,那如何通过Android搜索到这些设备?

模拟主机效果图:
Android网络编程TCP、UDP(三)——UDP实例:搜索局域网所有的设备_第1张图片

模拟设备效果图:
Android网络编程TCP、UDP(三)——UDP实例:搜索局域网所有的设备_第2张图片

3.6.1 原理分析

这些设备在局域网内,肯定是通过DHCP(Dynamic Host Configuration Protocol,动态主机配置协议)来获取内网IP的。也就是说每个设备的IP都不是固定的。而我们主要目的就是要获取这些设备的IP地址。

也许你说,把手机设置成一个固定的内网IP,然后让这些设备来连接这个固定IP。看上去OK啊,但万一这个IP被占用了,怎办?

每个设备的IP会变,但通信端口我们肯定可以固定。这就可以运用上面的UDP广播(或组播)技术。具体流程:

  1. 主机(Android手机)发送广播信息,并指定对方接收端口为devicePort;
  2. 自己的发送端口为系统分配的hostPort,封装在DatagramSocket中,开始监听此端口。防丢失,一共发三次,每次发送后就监听一段时间;
  3. 设备监听devicePort端口,收到信息后。首先解析数据验证是否是自己人(协议)发过来的,否,扔;是,则通过数据报获取对方的IP地址与端口hostPort;
  4. 设备通过获取到的IP地址与端口hostPort,给主机发送响应信息;
  5. 主机收到设备的响应,就可以知道设备的IP地址了。同时主机返回确认信息给设备,防止设备发给主机的响应信息丢失,毕竟是UDP;
  6. 有了IP地址,就可以为所欲为了,比如:建立安全连接TCP。

本解决方法有以下特点:

  • 灵活性高。主机使用系统自动分配端口,不用担心端口被其他软件占用;
  • 搜索迅速。使用了UDP广播,所有局域网内的设备几乎同时可以接受到信息;
  • 连接安全。为了避免UDP的不安全性,使用了类似TCP的三次握手;
  • 数据安全。加入了协议,提高了数据的安全性。

下面是广播实现的代码,当然也可以用组播来实现。组播因为组播地址的原因,可以进一步加强安全性,代码中把广播的网络那块改成组播就好了。(组播参考:Android网络编程TCP、UDP(二))

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工程不行。

尝试了很多方法找原因,如在命令行执行下面的命令,然而无终而返:

  1. 查看局域网中其他运行的电脑:net view
  2. 路由追踪:tracert (ip)
    eg:tracert baidu.com
    tracert 192.168.1.10
  3. 显示当前TCP/IP网络连接:netstat

最后终于找到解决办法,在“防火墙”的“允许的应用”中需要设置权限。把“Java(TM) Platform SE binary”勾上,并把后面的“专用”和“公用”网络都勾选上:
Android网络编程TCP、UDP(三)——UDP实例:搜索局域网所有的设备_第3张图片

4.2 局域网内只有有线连接的设备可以通信,无线设备却无法通信

其实问题详细情况是这样的:无线Wifi连接的设备不能与无线设备通信(内网IP通信),只能与有线设备通信;而有线设备一切正常。

这问题也很郁闷,查了资料也没有找到解决办法。但个人感觉这问题肯定是路由器的问题,因为局域网的控制系统就是路由器。幸运的是,我有两个一模一样的路由器,另一个路由器应该的。然后两台路由器,分别连两台电脑,通过电脑对路由器界面进行对比(英文是硬伤啊⊙﹏⊙b)。

最后锁定了这个东西“Wireless Isolation within SSID”,就是连接SSID的设备都独立,无法进行局域网内通信。曾经手滑了一下,点成Enable。改回Disabled,兄弟间就别分开了:
Android网络编程TCP、UDP(三)——UDP实例:搜索局域网所有的设备_第4张图片


本来一篇想搞定的,结果写了三篇,目录还是按原来一篇的来写,有点儿乱(^__^) ……

网络编程,终于赶在这个周末结束前告一段落。

你可能感兴趣的:(Android)