4

我在使用 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)

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

  • 启用Reuse address的功能(见代码) - 没有运气
  • 强制使用 IPv4(见代码) - 一样,没有运气
  • 在循环中,检查端口范围(32100 - 32110) - 也无济于事。所有端口也抛出相同的异常java.net.BindException: Address already in use
  • 硬编码 IP 的“0.0.0.0”和“10.1.xx”(见代码)——相同
  • 重新启动设备,更改 WiFi 网络 - 也无济于事

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

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

还有什么注意到 - 在我的带有 Android 7.1.1 的 Nexus 和 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;
        }

    }

}
4

1 回答 1

2

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

32100-32600,40100-40150

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

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

ip_local_reserved_ports- 逗号分隔范围列表

指定为已知第三方应用程序保留的端口。这些端口不会被自动端口分配使用(例如,在调用connect()bind()使用端口号 0 时)。显式端口分配行为没有改变

因此,当您将端口号显式传递给bind方法时,它仍然应该可以使用这些端口。显然这行不通。在我看来,Android 中使用的 Linux 内核实现提供的网络堆栈中存在一个错误。但这需要额外的调查。

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

于 2017-04-06T13:51:25.810 回答