Подтвердить что ты не робот

Android SSLServerSocket вызывает увеличение собственной памяти в приложении, OOM

Фон

Я разрабатываю приложение для Android, которое обеспечивает простой HTTP/HTTPS-сервер. Если настройка HTTPS настроена, то на каждом соединении наблюдается увеличение использования внутренней памяти, что в конечном итоге приводит к сбою приложения (oom), в то время как использование конфигурации HTTP сохраняет относительную постоянную память в исходной памяти. Приложение Java VM сохраняет относительную константу в обеих конфигурациях.

Приложение обслуживает HTML-страницу, содержащую javascript с периодическим опросом (один опрос json каждую секунду), поэтому вызов страницы приложения с использованием конфигурации HTTPS и сохранение открытой страницы в течение нескольких часов приведет к тому, из-за увеличения использования собственной памяти. Я испытал множество конфигураций SSLServerSocket и SSLContext, найденных в Интернете, без везения.

Я наблюдаю ту же проблему на разных Android-устройствах и разных версиях Android, начиная с 2.2 до 4.3.

Код для обработки клиентских запросов одинаковый для обеих конфигураций HTTP/HTTPS. Единственная разница между этими двумя конфигурациями - это настройка серверного сокета. В то время как в случае сокета HTTP-сервера одна единственная строка похожа на эту "ServerSocket serversocket = new ServerSocket (myport);" выполняет ли задание, в случае установки сервера HTTPS принимаются обычные шаги для настройки SSLContext - то есть настройка ключа-манипулятора и инициализация SSLContext. На данный момент я использую TrustManager по умолчанию.

Потребность в советах

Знает ли кто-нибудь о проблемах с утечкой памяти в Android-провайдере TLS по умолчанию, используя OpenSSL? Есть ли что-то особенное, которое я должен рассмотреть, чтобы избежать утечки в собственной памяти? Любые намеки приветствуются.

Обновление. Я также попробовал оба поставщика TLS: OpenSSL и JSSE, явно указав имя поставщика в SSLContext.getInstance( "TLS", имя поставщика). Но это ничего не изменило.

Вот блок кода, который демонстрирует проблему. Просто создайте пример приложения, разместив его в нижней части основного действия onCreate и создайте и запустите приложение. Убедитесь, что ваш Wi-Fi включен и вызывает страницу HTML по следующему адресу:

https://android device IP:9090

Затем просмотрите журналы adb, через некоторое время вы увидите, что начальная память начинает увеличиваться.


new Thread(new Runnable() {

public void run() {

final int PORT = 9090; SSLContext sslContext = SSLContext.getInstance( "TLS" ); // JSSE and OpenSSL providers behave the same way KeyManagerFactory kmf = KeyManagerFactory.getInstance( KeyManagerFactory.getDefaultAlgorithm() ); KeyStore ks = KeyStore.getInstance( KeyStore.getDefaultType() ); char[] password = KEYSTORE_PW.toCharArray(); // we assume the keystore is in the app assets InputStream sslKeyStore = getApplicationContext().getResources().openRawResource( R.raw.keystore ); ks.load( sslKeyStore, null ); sslKeyStore.close(); kmf.init( ks, password ); sslContext.init( kmf.getKeyManagers(), null, new SecureRandom() ); ServerSocketFactory ssf = sslContext.getServerSocketFactory(); sslContext.getServerSessionContext().setSessionTimeout(5); try { SSLServerSocket serversocket = ( SSLServerSocket )ssf.createServerSocket(PORT); // alternatively, the plain server socket can be created here //ServerSocket serversocket = new ServerSocket(9090); serversocket.setReceiveBufferSize( 8192 ); int num = 0; long lastnatmem = 0, natmemtotalincrease = 0; while (true) { try { Socket soc = (Socket) serversocket.accept(); Log.i(TAG, "client connected (" + num++ + ")"); soc.setSoTimeout(2000); try { SSLSession session = ((SSLSocket)soc).getSession(); boolean valid = session.isValid(); Log.d(TAG, "session valid: " + valid); OutputStream os = null; InputStream is = null; try { os = soc.getOutputStream(); // just read the complete request from client is = soc.getInputStream(); int c = 0; String itext = ""; while ( (c = is.read() ) > 0 ) { itext += (char)c; if (itext.contains("\r\n\r\n")) // end of request detection break; } //Log.e(TAG, " req: " + itext); } catch (SocketTimeoutException e) { // this can occasionally happen (handshake timeout) Log.d(TAG, "socket timeout: " + e.getMessage()); if (os != null) os.close(); if (is != null) is.close(); soc.close(); continue; } long natmem = Debug.getNativeHeapSize(); long diff = 0; if (lastnatmem != 0) { diff = natmem - lastnatmem; natmemtotalincrease += diff; } lastnatmem = natmem; Log.i(TAG, " answer the request, native memory in use: " + natmem / 1024 + ", diff: " + diff / 1024 + ", total increase: " + natmemtotalincrease / 1024); String html = "<!DOCTYPE html><html><head>"; html += "<script type='text/javascript'>"; html += "function poll() { request(); window.setTimeout(poll, 1000);}\n"; html += "function request() { var xmlHttp = new XMLHttpRequest(); xmlHttp.open( \"GET\", \"/\", false ); xmlHttp.send( null ); return xmlHttp.responseText; }"; html += "</script>"; html += "</head><body onload=\"poll()\"><p>Refresh the site to see the inreasing native memory when using HTTPS: " + natmem + " </p></body></html> "; byte[] buffer = html.getBytes("UTF-8"); PrintWriter pw = new PrintWriter( os ); pw.print("HTTP/1.0 200 OK \r\n"); pw.print("Content-Type: text/html\r\n"); pw.print("Content-Length: " + buffer.length + "\r\n"); pw.print("\r\n"); pw.flush(); os.write(buffer); os.flush(); os.close(); } catch (IOException e) { e.printStackTrace(); } soc.close(); } catch (IOException e) { e.printStackTrace(); } } } catch (SocketException e) { e.printStackTrace(); } catch (IOException e) { e.printStackTrace(); } } }).start();

- EDIT -

Я загрузил примерный проект приложения под названием SSLTest для eClipse, который демонстрирует проблему:

http://code.google.com/p/android/issues/detail?id=59536

- ОБНОВЛЕНИЕ -

Хорошие новости: сегодня сообщалось о выпуске Android выше, и были исправлены соответствующие исправления утечки памяти. Подробнее см. Ссылку выше.

4b9b3361

Ответ 1

Я предполагаю, что это будет значительная инвестиция времени, но я вижу, что Valgrind был перенесен на Android. Вы могли бы попробовать его запустить и запустить. Конечно, если вы обнаружите утечку внутренней памяти, вы не можете сделать ничего, кроме как попытаться исправить ошибку в будущих версиях Android.

В качестве обходного решения вы можете сделать свое приложение многопроцессором и поместить службу https в отдельный процесс. Таким образом, вы можете периодически перезапускать его, избегая OOM. Возможно, вам также понадобится третий процесс, который просто принимает подключения к порту 443 и передает их сотруднику https, чтобы избежать крошечных отключений, когда перезагружается рабочий стол https.

Это также звучит как значительная временная инвестиция:) Но это, вероятно, успешно устранит проблему.

--- EDIT: Подробнее ---

Да, если у вас есть основное приложение с собственным пользовательским интерфейсом, рабочий процесс для обработки SSL и рабочий процесс для приема запросов SSL (который, как вы говорите, вероятно, не может быть 443), тогда поверх вашего обычного Классы активности, у вас будет два класса Service, и манифест будет размещать их в отдельных процессах.

Обработка процесса SSL: вместо того, чтобы ожидать, что OOM приведет к сбою службы, служба может контролировать свой собственный Debug.getNativeHeapSize() и явно перезапускать службу, когда она слишком сильно увеличилась. Либо это, либо перезапустите автоматически после каждых 100 запросов или около того.

Обработка процесса прослушивания сокетов. Эта служба просто прослушивает выбранный вами TCP-порт и передает необработанные данные процессу SSL. Этот бит требует некоторой мысли, но наиболее очевидным решением является просто прослушивание SSL-процессов на другом локальном порту X (или переключение между различными портами), и процесс прослушивания сокетов пересылает данные в порт X. Причина для того, чтобы процесс прослушивающего сокета был изящно обработан возможностью того, что X не работает - как это может быть, когда вы его перезагружаете.

Если ваши требования допускают случайные мини-отключения, я бы просто обработал процесс обработки SSL и пропустил процесс прослушивания сокетов, это относительно простое решение - не так уж и отличается от того, что вы делали бы нормально. Это процесс прослушивания сокетов, который усложняет решение...

Ответ 2

Помогает ли это явно закрыть входной поток? В примере кода входной поток кажется закрытым только в случае исключения SocketTimeoutException.

- EDIT -

Вы можете переименовать run() в run2() и переместить цикл while в run() и удалить его из run2() и посмотреть, не изменилось ли это? Это не могло быть решением, но могло бы сказать вам, если какой-либо из долгоживущих объектов освободит память, когда их ссылки будут удалены.

Ответ 3

Есть одна деталь, которую я бы рекомендовал изменить в своей реализации.

Составьте список всех ваших переменных ресурсов, например, Sockets, Streams, Writers и т.д. Обязательно запишите выражение вне вашего оператора try и обязательно сделайте очистку/закрытие в заявлении finally. Я обычно делаю что-то вроде этого, чтобы быть на 100% уверенным:

InputStream in = null;
OutputStream out = null;

try {
    //assign a proper value to in and out, and use them as needed.
} catch(IOException e) {
    //normal error handling
} finally {
    try {
        in.close();
    } catch(IOException e) {}
    try {
        out.close();
    } catch(IOException e) {}
}

Это выглядит немного запутанным, но представьте, что вы используете свой Stream в блоке try, и вы получаете некоторое исключение, тогда ваши потоки никогда не закрываются, и это потенциальная причина утечек памяти.

Я не могу гарантировать, что это причина, но она должна быть хорошей точкой запуска.

Об управлении вашим сервисом. У меня было много неприятностей с услугами Android, потому что я запускал их в том же потоке, что и графический интерфейс. В некоторых случаях Android увидит некоторый код, который выполняется слишком долго и убивает ваш основной процесс, чтобы защитить от сбоев. Решение, которое я нашел, состояло в том, чтобы следовать предложению из этого урока (см. Пункт 4):

http://www.vogella.com/articles/AndroidServices/article.html

После этого моя служба просто работала так, как ожидалось, и не мешала моему графическому интерфейсу.

Привет