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

Тонкая производительность Java-сокетов

Я создал приложение управления удаленным рабочим столом. Очевидно, что он состоит из клиентских и серверных частей:

Сервер:

  • Получение действий мыши/клавиатуры от клиента;
  • Отправка снимок экрана с рабочего стола на клиент.

Клиент:

  • Получение снимка экрана с сервера;
  • Отправка действий мыши/клавиатуры;

Рассмотрите отправку снимка экрана. Когда я использую свой домашний компьютер в качестве сервера, я получаю размер скриншота 1920x1080. Используя JAI Image I/O Tools и кодируя его как PNG, я смог получить следующую статистику для такого большого изображения:

  • Время записи ~ 0,2 с; (не в сокет, а в некоторый "обычный" выходной поток, i. e. encode time)
  • Время чтения ~ 0,05 с; (не из сокета, а из некоторого "обычного" входного потока, i. e. decode time)
  • Размер ~ 250 КБ;
  • Отличное качество.

В результате, согласно № 1, идеальным возможным FPS должно быть ~ 5.

К сожалению, я не могу достичь даже тех ~ 5 FPS, даже не 2 FPS. Я искал узкое место и выяснил, что запись/чтение в/из потоков ввода/вывода сокетов занимает до ~ 2 с (См. Приложение 1 и 2 для пояснения). Конечно, это неприемлемо.

Я немного исследовал эту тему - и добавил буферизацию потоков ввода-вывода сокетов (с BufferedInputStream и BufferedOutputStream) с обеих сторон. Я начал с размеров 64 КБ. Это действительно улучшило производительность. Но все равно не может быть как минимум 2 FPS! Кроме того, я экспериментировал с Socket#setReceiveBufferSize и Socket#setSendBufferSize, и были некоторые изменения в скорости, но я не знаю, как они себя ведут, поэтому я не знаю, какие значения использовать.

Посмотрите на код инициализации:

Сервер:

    ServerSocket serverSocket = new ServerSocket();
    serverSocket.setReceiveBufferSize( ? ); // #1
    serverSocket.bind(new InetSocketAddress(...));

    Socket clientSocket = serverSocket.accept();
    clientSocket.setSendBufferSize( ? ); // #2
    clientSocket.setReceiveBufferSize( ? ); // #3

    OutputStream outputStream = new BufferedOutputStream(
            clientSocket.getOutputStream(), ? ); // #4
    InputStream inputStream = new BufferedInputStream(
            clientSocket.getInputStream(), ? ); // #5

Клиент:

    Socket socket = new Socket(...);
    socket.setSendBufferSize( ? ); // #6
    socket.setReceiveBufferSize( ? ); // #7

    OutputStream outputStream = new BufferedOutputStream(
            socket.getOutputStream(), ? ); // #8
    InputStream inputStream = new BufferedInputStream(
            socket.getInputStream(), ? ); // #9

Вопросы:

  • Какие значения вы бы рекомендовали (для повышения производительности) для всех эти случаи и почему?
  • Просьба уточнить Socket#setReceiveBufferSize и Socket#setSendBufferSize.
  • Какие другие методы/методы вы можете посоветовать повысить производительность? такого приложения?
  • Skype обеспечивает качественную передачу данных в реальном времени - как они это делают?

Приложение 1: Добавление развернутого псевдокода чтения клиентского сокета (@mcfinnigan):

while(true) {
    // objectInputStream is wrapping socket buffered input stream.
    Object object = objectInputStream.readObject(); // <--- Bottleneck (without setting proper buffer sizes is ~2 s)

    if(object == null)
        continue;

    if(object.getClass() == ImageCapsule.class) {
        ImageCapsule imageCapsule = (ImageCapsule)object;

        screen = imageCapsule.read(); // <--- Decode PNG (~0.05 s)

        SwingUtilities.invokeLater(new Runnable() {
            @Override
            public void run() {
                repaint();
            }
        });
    }
}

Приложение 2: Добавление развернутого псевдокода записи на серверный сокет (@EJP):

while(true) {
    // objectOutputStream is wrapping socket buffered output stream.
    BufferedImage screen = ... // obtaining screenshot
    ImageCapsule imageCapsule = new ImageCapsule();

    imageCapsule.write(screen, formatName()); // <--- Encode PNG (~0.2 s)

    try {
        objectOutputStream.writeObject(imageCapsule); // <--- Bottleneck (without setting proper buffer sizes is ~2 s)
    }
    finally {
        objectOutputStream.flush();
        objectOutputStream.reset(); // Reset to free written objects.
    }
}

Вывод:

Спасибо за ваши ответы, особенно EJP - он сделал вещи более ясными для меня. Если вы похожи на меня - поиск ответов на то, как настроить производительность сокетов, вы обязательно должны проверить Сокеты TCP/IP в Java, второе издание: практическое руководство для программистов, особенно глава 6 "Under the Hood", в которой описывается, что происходит за кулисами классов *Socket, как управление и использование буферов отправки и получения (которые являются первичными ключами к производительности).

4b9b3361

Ответ 1

  • Время записи ~ 0,2 с;
  • Время чтения ~ 0,05 с;

Эти цели совершенно бессмысленны без учета латентности и пропускной способности промежуточной сети.

Размер ~ 250 КБ;

Отключить тему. Размер изображения зависит от вас, и он не имеет отношения к фактическому программированию, которое является целью этого сайта.

Отличное качество.

"Отличное качество" требует только того, чтобы вы не отбрасывали какие-либо биты, которые вы не получите в любом случае через TCP.

  serverSocket.setReceiveBufferSize( ? ); // #1

Определяет размер принимаемого буфера для всех принятых сокетов. Установите его настолько, насколько это возможно, более 64k, если это возможно.

socket.setSendBufferSize( ? ); // #6

Установите это как можно больше, по возможности более 64k.

    socket.setReceiveBufferSize( ? ); // #7

Как это принято сокет, вы уже сделали это выше. Удалить.

    OutputStream outputStream = new BufferedOutputStream(
            socket.getOutputStream(), ? ); // #8
    InputStream inputStream = new BufferedInputStream(
            socket.getInputStream(), ? ); // #9

По умолчанию для них 8k; пока у вас есть приличные размеры буфера сокета, которых должно быть достаточно.

Какие значения вы бы рекомендовали (для повышения производительности) для всех этих случаев и почему?

См. выше.

Просьба уточнить поведение Socket#setReceiveBufferSize() и Socket#setSendBufferSize().

Они управляют размером окна TCP. Это довольно заумная тема, но идея состоит в том, чтобы получить размер, по крайней мере равный продукту с задержкой полосы пропускания вашей сети, т.е. Пропускную способность в байтах/секунду раз задержка в секундах >= размер буфера в байтах.

Какие другие методы/методы вы можете посоветовать повысить производительность такого приложения?

Не расстраивайтесь со сном и выполняйте другие задания при отправке данных. Отправьте его так быстро, как вы можете в максимально сжатом цикле, который вы можете организовать.

Skype обеспечивает качественную передачу данных в реальном времени - как они это делают?

Отключить тему и, возможно, непознаваемо, если сотрудник Skype не захочет раздавать фирменные секреты здесь.

Ответ 2

  • Если клиент открывает новое TCP-соединение для каждого изображения, то медленный запуск TCP может вызвать медленность. → Использовать то же TCP-соединение для всех фреймов.

  • У TCP есть собственные буферы, которые используются оптимальным способом для TCP → BufferedOutputStream, дает иногда выгоду, а иногда и нет.

  • Обычно между снимками экрана изменяется только небольшая часть экрана. → Переносить только измененные части.

Ответ 3

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

Недостатком является более сложная обработка. Может быть, есть некоторые простые API-интерфейсы/кодеки видеокодеков?

Ответ 5

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

http://docs.oracle.com/javase/1.4.2/docs/api/java/net/Socket.html#setTcpNoDelay(boolean)

чтобы установить значение true, вы сможете ускорить загрузку сокета за счет увеличения потребления полосы пропускания.