openjdk.ruOpenJDK на русском

JEP 353: Reimplement the Legacy Socket API

Новая реализация устаревшего Socket API

ОтветственныйAlan Bateman
ТипFeature
ОбластьJDK
СтатусClosed / Delivered
Выпуск13
Компонентcore-libs / java.net
Обсуждениеnet dash dev at openjdk dot java dot net
ТрудоёмкостьS
РецензентыBrian Goetz, Chris Hegarty, Michael McMahon
ОдобренBrian Goetz
Создан2019/02/06 13:49
Обновлён2026/01/22 15:00
Задача8218559

Аннотация

Заменить внутреннюю реализацию API java.net.Socket и java.net.ServerSocket более простой и современной реализацией, которую легко сопровождать и отлаживать. Новую реализацию будет легко адаптировать для работы с потоками пользовательского режима (так называемыми fibers), которые сейчас исследуются в проекте Loom.

Мотивация

API java.net.Socket и java.net.ServerSocket, а также их внутренние реализации появились ещё в JDK 1.0. Реализация представляет собой смесь устаревшего кода на Java и C, который трудно сопровождать и отлаживать. В качестве буфера ввода-вывода реализация использует стек потока, и из-за этого подхода размер стека потока по умолчанию уже несколько раз приходилось увеличивать. Для поддержки асинхронного закрытия реализация использует нативную структуру данных, которая за эти годы стала источником неочевидных проблем с надёжностью и переносом на другие платформы. Кроме того, в реализации есть несколько проблем с конкурентностью, для корректного решения которых её нужно переработать. В будущем, где fibers будут приостанавливаться (park), а не блокировать потоки в нативных методах, текущая реализация не годится для своей задачи.

Описание

API java.net.Socket и java.net.ServerSocket делегируют все операции с сокетами классу java.net.SocketImpl — механизму Service Provider Interface (SPI), который существует с JDK 1.0. Встроенная реализация называется «plain» (простой) реализацией; её реализует непубличный класс PlainSocketImpl со вспомогательными классами SocketInputStream и SocketOutputStream. Класс PlainSocketImpl расширяют две другие внутренние реализации JDK, которые поддерживают соединения через прокси-серверы SOCKS и HTTP. По умолчанию Socket и ServerSocket создаются (иногда отложенно) с SocketImpl на основе SOCKS. В случае ServerSocket использование реализации SOCKS — это странность, оставшаяся со времён экспериментальной (и впоследствии удалённой) поддержки проксирования серверных соединений в JDK 1.4.

Новая реализация, NioSocketImpl, — это прямая замена для PlainSocketImpl. Она разработана так, чтобы её было легко сопровождать и отлаживать. Она использует ту же внутреннюю инфраструктуру JDK, что и реализация New I/O (NIO), поэтому ей не нужен собственный нативный код. Она интегрирована с существующим механизмом кэширования буферов, поэтому ей не нужно использовать стек потока для ввода-вывода. Она использует блокировки java.util.concurrent вместо методов synchronized, чтобы в будущем хорошо работать с fibers. В JDK 11 NIO-реализация SocketChannel и другие реализации SelectableChannel были в основном переписаны с той же целью.

Несколько замечаний о новой реализации:

  • SocketImpl — устаревший механизм SPI с очень неполной спецификацией. Новая реализация стремится быть совместимой со старой, воспроизводя неспецифицированное поведение и исключения там, где это применимо. Различия в поведении старой и новой реализаций подробно описаны ниже в разделе «Риски и допущения».

  • Операции с сокетами, использующие тайм-ауты (connect, accept, read), реализованы переводом сокета в неблокирующий режим и опросом сокета.

  • Механизм java.lang.ref.Cleaner используется для закрытия сокетов, когда SocketImpl удаляется сборщиком мусора, а сокет не был явно закрыт.

  • Обработка сброса соединения реализована так же, как в старой реализации, чтобы попытки чтения после сброса соединения единообразно завершались ошибкой.

ServerSocket изменён так, чтобы по умолчанию использовать NioSocketImpl (или PlainSocketImpl). Реализация SOCKS в нём больше не используется.

Реализации SocketImpl для поддержки прокси-серверов SOCKS и HTTP изменены так, чтобы они делегировали вызовы и могли работать как со старой, так и с новой реализацией.

Поддержка инструментирования ввода-вывода сокетов в Java Flight Recorder изменена так, чтобы не зависеть от SocketImpl: события ввода-вывода сокетов можно записывать при работе как с новой, так и со старой или пользовательской реализацией.

Чтобы снизить риск замены реализации, существовавшей более двадцати лет, старая реализация удалена не будет. Она останется в JDK, и будет добавлено системное свойство, с помощью которого JDK можно настроить на использование старой реализации. Специфичное для JDK системное свойство для переключения на старую реализацию — jdk.net.usePlainSocketImpl. Если при запуске оно задано или имеет значение true, будет использоваться старая реализация. В одном из будущих выпусков PlainSocketImpl и это системное свойство будут удалены.

Этот JEP пока не предлагает альтернативную реализацию DatagramSocketImpl (DatagramSocketImpl — внутренняя реализация, которой делегируют экземпляры java.net.DatagramSocket). Встроенная реализация по умолчанию (PlainDatagramSocketImpl) создаёт нагрузку на сопровождение (и перенос на другие платформы) и может стать предметом другого JEP.

Тестирование

Для тестирования новой реализации будут использоваться существующие тесты в репозитории jdk/jdk. За годы в группе тестов jdk_net накопилось много тестов для пограничных сценариев работы с сетью. Некоторые тесты из этой группы будут изменены так, чтобы запускаться дважды, второй раз с -Djdk.net.usePlainSocketImpl, чтобы старая реализация не деградировала в течение того времени, пока JDK содержит обе реализации.

Сегодня много кода прямо или косвенно использует библиотеки, которые используют API из java.nio.channels, а не API java.net.Socket и java.net.ServerSocket. Будут приложены все усилия, чтобы рассказать об этом предложении и побудить разработчиков, чей код использует Socket и ServerSocket, протестировать его на сборках Early Access (ранний доступ), публикуемых на jdk.java.net или в других местах.

Микробенчмарки в репозитории jdk/jdk включают бенчмарки чтения/записи и потоковой передачи через сокеты. Эти бенчмарки доработаны, чтобы старую и новую реализации было легко сравнивать. На данный момент в тестах чтения/записи через сокеты новая реализация показывает такие же результаты, как старая, или лучше на 1–3 %.

Риски и допущения

Основной риск этого предложения в том, что существует код, который зависит от неспецифицированного поведения в пограничных случаях, где старая и новая реализации ведут себя по-разному. Выявленные к настоящему времени различия перечислены ниже; все, кроме первых двух, можно устранить, запустив приложение с -Djdk.net.usePlainSocketImpl.

  • InputStream и OutputStream, возвращаемые методами getInputStream() и getOutputStream() класса PlainSocketImpl, расширяют java.io.FileInputStream и java.io.FileOutputStream соответственно. Возможно, хотя и маловероятно, что существует код, который от этого зависит.

  • ServerSocket, использующий пользовательский SocketImpl, не может принимать соединения, возвращающие Socket с платформенным SocketImpl. Аналогично, ServerSocket, использующий платформенный SocketImpl, не может принимать соединения, возвращающие Socket с пользовательским SocketImpl.

  • InputStream и OutputStream, возвращаемые старой реализацией, проверяют поток на EOF и возвращают -1 до других проверок. Новая реализация выполняет null и проверки границ до проверки того, достигнут ли конец потока (EOF). Возможно, хотя и маловероятно, что существует хрупкий код, который даст сбой из-за порядка проверок.

  • Закрытие Socket с непрочитанными байтами в очереди приёма корректно закроет внутренний сокет. На платформах, отличных от Microsoft Windows, тот же сценарий со старой реализацией приведёт к аварийному (жёсткому) закрытию.

  • Только для Oracle Solaris: Oracle Solaris отличается от других платформ тем, как сообщает приложениям о «сбросе соединения» (connection reset). Например, вызовы setsockopt или ioctl могут завершиться ошибкой при сетевой ошибке. Чтобы отключить это поведение, можно настроить параметр xnet_skip_checks в /etc/system (echo "xnet_skip_checks/W 1" | mdb -kw в работающей системе). Старая реализация обрабатывает случай, когда ioctl(FIOREAD) завершается ошибкой, чтобы попытки чтения после ошибки available с «сбросом соединения» единообразно завершались ошибкой. Это хрупко и неудобно в сопровождении, поэтому новая реализация не пытается воспроизводить такое поведение.

  • Только для Oracle Solaris: Oracle Solaris не позволяет изменять параметр сокета IPV6_TLCASS у TCP-сокета после установления соединения. Старая реализация скрывает это, кэшируя значение, переданное методу setTrafficClass.

  • В пакете java.net определено много подклассов SocketException. Новая реализация будет стараться выбрасывать те же конкретные SocketException, что и старая, но возможны случаи, когда они будут отличаться. Кроме того, возможны случаи, когда будут отличаться сообщения исключений. Например, в Microsoft Windows старая реализация сопоставляет коды ошибок Windows Socket сообщениям только на английском языке, а новая реализация использует системные сообщения.

Помимо различий в поведении, при определённых нагрузках производительность новой реализации может отличаться от старой. В старой реализации несколько потоков, вызывающих метод accept у ServerSocket, выстраиваются в очередь в ядре. В новой реализации один поток будет заблокирован в системном вызове accept, а остальные будут стоять в очереди в ожидании захвата блокировки java.util.concurrent. Характеристики производительности могут отличаться и в других сценариях.

Наконец, могут существовать агенты или инструменты инструментирования, которые инструментируют непубличные классы java.net.SocketInputStream и java.net.SocketOutputStream для получения событий ввода-вывода. Новая реализация эти классы не использует.