Проблема с DatagramSocket на Android 7.1.1

Я столкнулся с очень странной проблемой в Google Pixel QA с Android 7.1.1 (N_MR1). Мы используем UDP-сервер и клиент для рукопожатия во время установления TCP-соединения.

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.x.x" (см. код) - одинаковые
  • перезагружал устройство, менял WiFi сеть - тоже не помогло

Также я проверил, кто использует порты на устройстве (приложение 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;
        }

    }

}

person Serhii.Komlach    schedule 04.04.2017    source источник
comment
Еще две вещи, касающиеся вашего кода: 1) isActive должен быть объявлен как volatile. 2) Подумайте об использовании здесь Thread вместо AsyncTask, поскольку последний повторно использует потоки из пула потоков и может в конечном итоге заблокироваться (что на самом деле произошло со мной во время выключения сервера)   -  person Idolon    schedule 06.04.2017


Ответы (1)


Проблема в порте, который вы используете. На моем телефоне Pixel в файле /proc/sys/net/ipv4/ip_local_reserved_ports определены следующие диапазоны портов:

32100-32600,40100-40150

Если я изменю номер порта в вашем коде на что-либо за пределами этого диапазона (и, конечно, выше 1024), он будет работать нормально, и я смогу отправлять данные в приложение с другого хоста.

Ядро Linux документация описывает этот файл следующим образом:

ip_local_reserved_ports - список диапазонов, разделенных запятыми

Укажите порты, зарезервированные для известных сторонних приложений. Эти порты не будут использоваться при автоматическом назначении портов (например, при вызове connect() или bind() с номером порта 0). Поведение явного выделения портов не изменилось.

Таким образом, когда вы явно передаете номер порта методу bind, он все равно должен использовать эти порты. Видимо это не работает. На мой взгляд, где-то в сетевом стеке есть ошибка, предоставляемая реализацией ядра Linux, используемой в Android. Но это требует дополнительного расследования.

Вам также может пригодиться следующий список содержимого ip_local_reserved_ports на разных телефонах: https://census.tsyrklevich.net/sysctls/net.ipv4.ip_local_reserved_ports

person Idolon    schedule 06.04.2017