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

JEP 373: Reimplement the Legacy DatagramSocket API

Новая реализация унаследованного API DatagramSocket

ОтветственныйDaniel Fuchs
ТипFeature
ОбластьJDK
СтатусClosed / Delivered
Выпуск15
Компонентcore-libs / java.net
Обсуждениеnet dash dev at openjdk dot java dot net
ТрудоёмкостьS
РецензентыAlan Bateman, Brian Goetz, Chris Hegarty
ОдобренBrian Goetz
Создан2019/12/10 16:13
Обновлён2023/03/04 02:10
Задача8235674

Аннотация

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

Мотивация

Кодовая база API java.net.DatagramSocket и java.net.MulticastSocket и их внутренних реализаций старая и хрупкая:

  • Реализации появились ещё в JDK 1.0. Это смесь унаследованного кода на Java и C, которую трудно сопровождать и отлаживать.

  • Реализация MulticastSocket особенно проблематична, поскольку она появилась в то время, когда IPv6 ещё находился в разработке. Значительная часть внутренней нативной реализации пытается согласовать IPv4 и IPv6 способами, которые трудно сопровождать.

  • Кроме того, в реализации есть ряд проблем с многопоточностью (например, при асинхронном закрытии), для корректного устранения которых требуется её капитальная переработка.

Кроме того, в контексте Virtual Threads, которые при системных вызовах паркуются, а не блокируют нижележащие потоки ядра, текущая реализация не отвечает своему назначению. Поскольку транспорты на основе датаграмм снова набирают популярность (например, QUIC), нужна более простая и удобная в сопровождении реализация.

Описание

Сейчас классы DatagramSocket и MulticastSocket делегируют все вызовы сокета реализации java.net.DatagramSocketImpl, для которой существуют разные конкретные реализации под каждую платформу: PlainDatagramSocketImpl на платформах Unix, а также TwoStackPlainDatagramSocketImpl и DualPlainDatagramSocketImpl на платформах Windows. Абстрактный класс DatagramSocketImpl, появившийся ещё в JDK 1.1, специфицирован очень слабо и содержит несколько устаревших методов, которые мешают создать реализацию этого класса на основе NIO (см. альтернативы, рассмотренные ниже).

Вместо того чтобы предоставить прямую замену для реализаций DatagramSocketImpl, как это было сделано в JEP 353 для SocketImpl, этот JEP предлагает, чтобы DatagramSocket внутри себя оборачивал другой экземпляр DatagramSocket, которому напрямую делегирует все вызовы. Обёрнутый экземпляр — это либо адаптер сокета, созданный из NIO DatagramChannel::socket (новая реализация), либо клон унаследованного класса DatagramSocket, который затем делегирует вызовы унаследованной реализации DatagramSocketImpl (чтобы реализовать переключатель для обратной совместимости). Если приложение установило DatagramSocketImplFactory, выбирается старая унаследованная реализация. В противном случае выбирается новая реализация, и она используется по умолчанию.

Чтобы снизить риск замены реализации, существующей более двадцати лет, унаследованная реализация удалена не будет. Вводится специфичное для JDK системное свойство jdk.net.usePlainDatagramSocketImpl, с помощью которого JDK можно настроить на использование унаследованной реализации (см. риски и допущения ниже). Если при запуске свойство задано без значения или со значением ”true", используется унаследованная реализация. В противном случае используется новая реализация (на основе NIO). В одном из будущих выпусков мы удалим унаследованную реализацию и системное свойство. Со временем мы также можем пометить DatagramSocketImpl и DatagramSocketImplFactory как Deprecated (устаревший) и удалить их.

Новая реализация включена по умолчанию. Она обеспечивает непрерываемое поведение для датаграммных и многоадресных сокетов, напрямую используя реализацию провайдера селекторов, принятую на платформе по умолчанию (sun.nio.ch.SelectorProviderImpl и sun.nio.ch.DatagramChannelImpl). Поэтому установка собственного провайдера селекторов не повлияет на DatagramSocket и MulticastSocket.

Альтернативы

Мы исследовали, реализовали в виде прототипов и отвергли два альтернативных подхода.

Альтернатива 1

Создать реализацию DatagramSocketImpl, которая делегирует все свои вызовы обёрнутым DatagramChannel и sun.nio.ch.DatagramSocketAdaptor. Доработать sun.nio.ch.DatagramSocketAdaptor, чтобы он расширял java.net.MulticastSocket.

Этот подход показал, что создать реализацию DatagramSocketImpl на основе DatagramChannel относительно легко. Тесты проходили, но при этом выявился и ряд ограничений:

  • Проверки безопасности выполнялись дважды: один раз в DatagramSocket и ещё раз в DatagramChannel (или в его адаптере сокета). Двойных проверок безопасности можно было избежать, но это было бы громоздко.

  • Мешала и эмуляция соединения, реализованная на уровне DatagramSocket, поскольку мы не хотели выполнять эту эмуляцию в реализации на основе NIO.

  • Как и у предложенного выше решения, главное преимущество этой альтернативы по сравнению со второй альтернативой, описанной ниже, состояло в том, что новый нативный код не требовался, поскольку каждый вызов можно было делегировать DatagramChannel.

  • При оценке этой альтернативы быстро стало ясно, что переопределять методы на уровне DatagramSocket, а не на уровне DatagramSocketImpl, было бы проще и прямолинейнее. Это и привело к решению, предложенному в этом JEP.

Альтернатива 2

Создать в пакете sun.nio.ch реализацию DatagramSocketImpl, которая вызывает низкоуровневые примитивы sun.nio.ch.Net. Так реализация могла напрямую обращаться к низкоуровневым примитивам NIO, а не полагаться на DatagramChannel. Это было отчасти аналогично тому, что было сделано при новой реализации Socket и ServerSocket в JEP 353.

  • Главное преимущество этой альтернативы перед первой состояло в том, что она позволяла избежать двойных проверок безопасности, поскольку реализация могла напрямую обращаться к низкоуровневым примитивам NIO.

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

  • Кроме того, требовалось добавить новый нативный код, соответствующий интерфейсу DatagramSocketImpl.

  • Поэтому решение, предложенное в этом JEP, оказалось гораздо проще, менее рискованным и более удобным в сопровождении.

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

Для тестирования новой реализации будут использованы существующие тесты в репозитории jdk/jdk. Чтобы переход прошёл гладко, новая реализация должна пройти набор регрессионных тестов tier2 (jdk_net и jdk_nio) и JCK для java_net/api. За годы в группе тестов jdk_net накопилось много тестов для граничных сценариев работы с сетью. Некоторые тесты из этой группы будут изменены так, чтобы запускаться дважды, второй раз с -Djdk.net.usePlainDatagramSocketImpl, — это гарантирует, что старая реализация не деградирует, пока JDK содержит обе реализации. При необходимости будут добавлены новые тесты, чтобы расширить покрытие кода и повысить уверенность в новой реализации.

Будут приложены все усилия, чтобы широко оповестить о предложении и побудить разработчиков, чей код использует DatagramSocket и MulticastSocket, протестировать его на сборках Early Access (ранний доступ), публикуемых на jdk.java.net.

Микробенчмарки в репозитории jdk/jdk включают бенчмарки для DatagramChannel. Аналогичные бенчмарки для датаграммных сокетов будут созданы, если их нет, или обновлены, если они уже существуют, так, чтобы старую и новую реализации было легко сравнить.

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

Основной риск этого предложения в том, что существует код, зависящий от неспецифицированного поведения в граничных случаях, где старая и новая реализации ведут себя по-разному. Чтобы свести этот риск к минимуму, в JDK 14 и JDK 15 уже была проведена подготовительная работа по уточнению спецификаций DatagramSocket и MulticastSocket и по сокращению различий в поведении между этими классами и адаптером DatagramChannel::socket. Однако некоторые небольшие различия, перечисленные ниже, могут сохраниться. Эти различия могут проявляться в граничных ситуациях, но для подавляющего большинства пользователей API они должны быть незаметны. Выявленные на данный момент различия перечислены здесь; все, кроме первых двух, можно устранить, запустив программу с -Djdk.net.usePlainDatagramSocketImpl или -Djdk.net.usePlainDatagramSocketImpl=true.

  • Собственные API или подклассы DatagramSocket и MulticastSocket, синхронизирующиеся на экземплярах этих классов, возможно, придётся пересмотреть, поскольку DatagramSocket и MulticastSocket больше не синхронизируются на this. Все блокировки и синхронизация оставлены на усмотрение делегата, который недоступен вне пакета java.net и может использовать любой механизм, какой сочтёт подходящим.

  • Аналогично, у собственных классов, которые расширяют DatagramSocket или MulticastSocket и переопределяют такие методы, как bind и setReuseAddress, переопределённые методы не будут вызываться при конструировании. Тот, кто так делает, полагается на недокументированное поведение, специфичное для реализации.

  • Новая реализация на всех платформах использует нативный метод connect. Унаследованная реализация на macOS по-прежнему использует эмуляцию. В частности, это означает, что старая реализация не может обнаружить недоступность порта, тогда как новая должна её обнаруживать. Кроме того, если нативное соединение не удаётся, старая реализация переходит к использованию эмуляции, а новая вместо этого сообщит об ошибке. Кроме того, новая реализация очищает буфер приёма в момент соединения, так что все датаграммы, помещённые в буфер до вызова connect, отбрасываются. Старая реализация сохраняла датаграммы, отправленные подключённым узлом и помещённые в буфер до того, как ядро установило связь, а новая реализация будет просто отбрасывать их.

  • На macOS и Linux вызов disconnect в новой реализации может потребовать повторной привязки нижележащего сокета. Из-за этого повторная привязка может завершиться неудачей, и нижележащая реализация может выбросить исключение, оставив нижележащий сокет в неспецифицированном состоянии. Если унаследованная реализация могла молча оставить сокет в неспецифицированном состоянии, то новая реализация вместо этого выбросит UncheckedIOException.

  • При присоединении к многоадресной группе на macOS, если исходящий интерфейс по умолчанию не задан и исходящий сетевой интерфейс не указан, старая реализация MulticastSocket::joinGroup выбирает сетевой интерфейс по умолчанию и перед присоединением ошибочно пытается сделать его интерфейсом по умолчанию, молча устанавливая опцию IP_MULTICAST_IF. Новая реализация на основе NIO этого не делает, поэтому опция IP_MULTICAST_IF никогда не будет молча устанавливаться как побочный эффект присоединения.

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

Другие заметные различия в поведении:

  • DatagramSocket, созданный с помощью одного из его публичных конструкторов, поддерживает установку опций для отправки многоадресных датаграмм. Новая реализация на всех платформах позволяет настраивать опции многоадресного сокета на базовых экземплярах DatagramSocket. Старая реализация в Windows по-прежнему использует реализацию с двойным стеком, которая не поддерживает опции многоадресного сокета на базовых экземплярах DatagramSocket. В этом случае, если такие опции нужно настроить, необходимо использовать экземпляр MulticastSocket.

  • Новая реализация исправляет ряд проблем, например 8165653, просто благодаря делегированию реализации NIO, в которой этих проблем нет.

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

Зависимости

  • Замена внутренней реализации DatagramSocket и MulticastSocket — необходимое условие для проекта Loom.