Android 7.1.1 上的 DatagramSocket 问题

Issue with DatagramSocket on Android 7.1.1

我在使用 Android 7.1.1 (N_MR1) 的 QA Google Pixel 上遇到了一个非常奇怪的问题。 我们在建立TCP连接时使用UDP Server和Client进行握手。

QA 报告 Pixel 握手无效。探索 Logcat 后我发现 UdpServerTask 抛出异常:

java.net.BindException: Address already in use
at java.net.PlainDatagramSocketImpl.bind0(Native Method)
at java.net.AbstractPlainDatagramSocketImpl.bind(AbstractPlainDatagramSocketImpl.java:96)
at java.net.DatagramSocket.bind(DatagramSocket.java:387)

到目前为止我尝试了什么:

此外,我检查了谁使用设备上的端口(NetStat+ 应用程序)- IP 和端口是免费的,没有人使用。但是当我试图调用 bind() - 异常发生了。

同时 UDP 客户端(按需调用)工作正常 - 我可以通过目标端口发送 UDP 数据包。

另外注意到 - 在我的 Nexus Android 7.1.1 和较低 Android 版本的设备上我无法重现该问题。

测试示例

public class UDPServer {

    int PORT = 32100;
    long TIMEOUT = 30000;

    private void log(String msg) {
        System.out.println(msg);
    }

    private boolean isActive = false;
    public ArrayList<UdpServerTask> tasks = new ArrayList<>();

    public void process(final byte[] data) {
        AsyncTask<Void, Void, Void> loadTask = new AsyncTask<Void, Void, Void>() {

            @Override
            protected Void doInBackground(Void... params) {
                //process data
                return null;
            }

        };

        Utils.executeTask(loadTask);
    }

    public void startAddress(String host) {
        UdpServerTask loadTask = new UdpServerTask(host, PORT);
        tasks.add(loadTask);
        Utils.executeTask(loadTask);
    }


    public void runUdpServer() {
        java.lang.System.setProperty("java.net.preferIPv6Addresses", "false");
        java.lang.System.setProperty("java.net.preferIPv4Stack", "true");
        stop_UDP_Server();
        isActive = true;
        AsyncTask<Void, Void, Void> mainTask = new AsyncTask<Void, Void, Void>() {
            ArrayList<String> ips = new ArrayList<>();

            @Override
            protected Void doInBackground(Void... params) {
                log("UDP starting servers ");
                ips.add(null);
                ips.add("0.0.0.0");
                try {
                    Enumeration<NetworkInterface> interfaces = NetworkInterface.getNetworkInterfaces();
                    while (interfaces.hasMoreElements()) {
                        NetworkInterface networkInterface = interfaces.nextElement();

                        if (networkInterface.isLoopback() || !networkInterface.isUp()) {
                            continue;
                        }
                        for (InterfaceAddress interfaceAddress : networkInterface.getInterfaceAddresses()) {
                            InetAddress broadcast = interfaceAddress
                                    .getBroadcast();
                            if (broadcast == null || broadcast instanceof Inet6Address) {
                                continue;
                            }

                            if (!ips.contains(broadcast.getHostAddress())) {
                                ips.add(broadcast.getHostAddress());
                            }
                        }

                    }
                } catch (final Throwable e) {
                    e.printStackTrace();

                }
                return null;
            }

            @Override
            protected void onPostExecute(Void result) {
                for (String host : ips) {
                    startAddress(host);
                }

            }

        };

        Utils.executeTask(mainTask);

    }

    public boolean reallyStopped() {
        return !isActive && tasks.isEmpty();
    }

    public void stop_UDP_Server() {
        isActive = false;

        AsyncTask<Void, Void, Void> mainTask = new AsyncTask<Void, Void, Void>() {
            @Override
            protected Void doInBackground(Void... params) {
                log("UDP start stopping");

                for (UdpServerTask task : tasks) {
                    task.cancelServer();
                }

                tasks.clear();
                return null;
            }

        };

        Utils.executeTask(mainTask);

        while (!reallyStopped()) {
            try {
                Thread.sleep(100);
            } catch (Exception e) {
            }
        }

    }


    private class UdpServerTask extends AsyncTask<Void, Void, Void> {
        String ip;
        int port;

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

        DatagramSocket ds = null;

        public void cancelServer() {
            log("UDP server cancelServer");
            if (ds != null && !ds.isClosed()) {
                try {
                    ds.close();
                    ds = null;
                } catch (Exception e) {
                    e.printStackTrace();
                }
            }
            log("UDP server stopped");
        }

        @Override
        protected Void doInBackground(Void... params) {

            long time = System.currentTimeMillis();
            boolean firstAttempt = true;
            while (System.currentTimeMillis() - time <= TIMEOUT && isActive) {
                try {

                    if (ds != null && !ds.isClosed()) {
                        try {
                            ds.close();
                            ds = null;
                        } catch (Exception e) {
                            e.printStackTrace();
                        }
                    }

                    log("UDP try create connection " + this.ip + ":" + this.port);

                    if (firstAttempt) {
                        ds = new DatagramSocket(new InetSocketAddress(TextUtils.isEmpty(this.ip) ? null : InetAddress.getByName(this.ip), this.port));
                    } else {
                        ds = new DatagramSocket(null);
                    }

                    ds.setBroadcast(true);

                    if (!firstAttempt) {
                        ds.setReuseAddress(true);
                        ds.bind(new InetSocketAddress(TextUtils.isEmpty(this.ip) ? null : InetAddress.getByName(this.ip), this.port));
                    }

                    long start = System.currentTimeMillis();

                    while (!ds.isBound()) {
                        if (System.currentTimeMillis() - start >= TIMEOUT) {
                            throw new Exception("Cann't bind to " + this.ip + ":" + this.port);
                        }
                        Thread.sleep(150);
                    }

                    log("UDP Server Started on " + this.ip + ":" + this.port);
                    while (isActive) {
                        final byte[] lMsg = new byte[4096];
                        final DatagramPacket dp = new DatagramPacket(lMsg, lMsg.length);
                        ds.receive(dp);


                        log("process UDP from " + dp.getAddress().toString() + ":" + dp.getPort());
                        process(dp.getData());


                    }
                    log("UDP Server Stopped on " + this.ip + ":" + this.port);


                } catch (final Throwable e) {
                    e.printStackTrace();
                    firstAttempt = false;
                    log("UDP Server Failed " + this.ip + ":" + this.port + " " + e);
                    try {
                        Thread.sleep(TIMEOUT / 10);
                    } catch (Exception ex) {
                    }

                }
            }


            if (ds != null && !ds.isClosed())
                try {
                    ds.close();
                    ds = null;
                } catch (Exception e) {
                    e.printStackTrace();
                }

            log("UDP Server finish task");

            return null;
        }

    }

}

问题出在您使用的端口上。在我的 Pixel phone 上,/proc/sys/net/ipv4/ip_local_reserved_ports 文件中定义了以下端口范围:

32100-32600,40100-40150

如果我将您代码中的端口号更改为超出此范围的任何值(当然超过 1024),它工作正常并且我能够从其他主机向应用程序发送数据。

Linux 内核 documentation 是这样描述这个文件的:

ip_local_reserved_ports - list of comma separated ranges

Specify the ports which are reserved for known third-party applications. These ports will not be used by automatic port assignments (e.g. when calling connect() or bind() with port number 0). Explicit port allocation behavior is unchanged.

因此,当您明确地将端口号传递给 bind 方法时,它应该仍然可以使用这些端口。显然这不起作用。在我看来,Android 中使用的 Linux 内核实现提供的网络堆栈中某处存在错误。但这需要进一步调查。

您可能还会发现以下 ip_local_reserved_ports 内容列表对不同的 phone 有用: https://census.tsyrklevich.net/sysctls/net.ipv4.ip_local_reserved_ports