Оптимизация производительности сокетов Java

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

Сервер:

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

Клиент:

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

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

  1. Время записи ~0,2 с; (не в сокет, а в некий "обычный" поток вывода, т.е. время кодирования)
  2. Время чтения ~0,05 с; (не из сокета, а из какого-то "обычного" входного потока, т.е. время декодирования)
  3. Размер ~250 КБ;
  4. Идеальное качество.

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

К сожалению, я не могу добиться даже этих ~5 кадров в секунду, даже 2 кадров в секунду. Я поискал узкое место и обнаружил, что запись/чтение в/из потоков ввода/вывода сокетовзанимает до ~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

Вопросы:

  1. Какие значения вы бы рекомендовали (для повышения производительности) для всех эти случаи и почему?
  2. Уточните, пожалуйста, Socket#setReceiveBufferSizeи Поведение Socket#setSendBufferSize.
  3. Какие еще методы/приемы вы можете посоветовать для повышения производительности такого приложения?
  4. Skype обеспечивает качественную передачу данных в режиме реального времени на рабочий стол — как они это делают?

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

while(true) {
    // objectInputStream is wrapping socket's 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's 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 «Под капотом», в которой описывается что происходит за кулисами классов *Socket, как управляются и используются буферы отправки и получения (которые являются основными ключами к производительности).

18
задан Alexander Shukaev 20 May 2012 в 10:08
поделиться