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 для получения событий ввода-вывода. Новая реализация эти классы не использует.