JEP 373: Reimplement the Legacy DatagramSocket API | 重新实现遗留的 DatagramSocket API
摘要
将 java.net.DatagramSocket
和 java.net.MulticastSocket
API 的底层实现替换为更简单、更现代的实现,这些实现易于维护和调试。新的实现将易于适应与当前在 Project Loom 中探索的虚拟线程一起工作。这是对已经重新实现了传统 Socket API 的 JEP 353 的后续跟进。
动机
java.net.DatagramSocket
和 java.net.MulticastSocket
API 及其底层实现的代码库陈旧且脆弱:
这些实现可以追溯到 JDK 1.0。它们是难以维护和调试的遗留 Java 和 C 代码的混合体。
MulticastSocket
的实现尤其成问题,因为它可以追溯到 IPv6 仍在开发中的时代。许多底层本地实现试图以难以维护的方式协调 IPv4 和 IPv6。实现还存在一些并发问题(例如,异步关闭),需要彻底改造才能妥善解决。
此外,在虚拟线程上下文中,这些线程在系统调用中停车而不是阻塞底层内核线程,当前的实现并不适合目的。随着基于数据报的传输再次获得关注(例如 QUIC),需要一个更简单、更易维护的实现。
描述
目前,DatagramSocket
和 MulticastSocket
类将所有套接字调用委托给 java.net.DatagramSocketImpl
的实现,对于不同的平台,存在不同的具体实现:Unix 平台上的 PlainDatagramSocketImpl
,以及 Windows 平台上的 TwoStackPlainDatagramSocketImpl
和 DualPlainDatagramSocketImpl
。抽象的 DatagramSocketImpl
类可以追溯到 JDK 1.1,它的定义非常不完整,并包含多个已经过时的方法,这些方法是基于 NIO 实现这个类的一个障碍(见下文讨论的替代方案)。
与在 JEP 353 中为 SocketImpl
所做的类似替换不同,这个 JEP 提议让 DatagramSocket
在内部包装另一个 DatagramSocket
的实例,并直接将所有调用委托给它。被包装的实例要么是从 NIO DatagramChannel::socket
创建的套接字适配器(新实现),要么是传统 DatagramSocket
类的克隆,然后委托给传统的 DatagramSocketImpl
实现(用于实现向后兼容性的切换)。如果应用程序安装了 DatagramSocketImplFactory
,则会选择旧的传统实现。否则,默认情况下将选择并使用新实现。
为了减少在二十多年后切换实现的风险,旧的实现将不会被移除。引入了一个特定于 JDK 的系统属性 jdk.net.usePlainDatagramSocketImpl
,以配置 JDK 使用旧的实现(请参阅以下的风险和假设)。如果在启动时没有设置该值或设置为值 "true"
,则将使用旧的实现。否则,将使用新的(基于 NIO 的)实现。在未来的某个版本中,我们将移除旧的实现和系统属性。在某个时候,我们可能也会弃用并移除 DatagramSocketImpl
和 DatagramSocketImplFactory
。
新的实现是默认启用的。它通过使用选择器提供程序(sun.nio.ch.SelectorProviderImpl
和 sun.nio.ch.DatagramChannelImpl
)的平台默认实现,为数据报和多播套接字提供了不可中断的行为。因此,安装自定义的选择器提供程序对 DatagramSocket
和 MulticastSocket
没有影响。
备选方案
我们研究、原型设计和排除了两种备选方案。
备选方案 1
创建一个 DatagramSocketImpl
的实现,该实现将其所有调用委托给一个封装的 DatagramChannel
和 sun.nio.ch.DatagramSocketAdaptor
。将 sun.nio.ch.DatagramSocketAdaptor
升级为扩展 java.net.MulticastSocket
。
这种方法表明,基于 DatagramChannel
提供 DatagramSocketImpl
的实现相对容易。测试通过,但也突显出几个限制:
安全检查被进行了两次,一次在
DatagramSocket
中,另一次在DatagramChannel
(或其套接字适配器)中。虽然有方法可以避免双重安全检查,但这些方法会很繁琐。在
DatagramSocket
级别实现的连接模拟也妨碍了操作,因为我们不想在基于 NIO 的实现中进行此模拟。与上面提出的解决方案一样,与下面第二种备选方案相比,这种备选方案的主要优势在于不需要新的本地代码,因为每个调用都可以委托给
DatagramChannel
。在评估这种备选方案时,很快发现,在
DatagramSocket
级别而不是在DatagramSocketImpl
级别重写方法会更简单、更直接,这导致了本 JEP 中提出的解决方案。
备选方案 2
在 sun.nio.ch
包中创建一个 DatagramSocketImpl
的实现,该实现调用低级别的 sun.nio.ch.Net
原语。这使得实现可以直接访问较低级别的 NIO 原语,而不是依赖于 DatagramChannel
。这与在 JEP 353 中重新实现 Socket
和 ServerSocket
的做法有些类似。
与第一个备选方案相比,这种备选方案的主要优势在于它避免了双重安全检查,因为实现可以直接访问较低级别的 NIO 原语。
然而,新的实现必须复制
DatagramChannel
已经实现的非平凡状态和锁管理。它还需要添加新的本地代码以匹配
DatagramSocketImpl
接口。因此,本 JEP 中提出的解决方案看起来更简单、风险更低且更易于维护。
测试
将使用 jdk/jdk
仓库中的现有测试来测试新实现。为确保平稳过渡,新实现应通过 tier2(jdk_net
和 jdk_nio
)回归测试套件以及 java_net/api
的 JCK 测试。多年来,jdk_net
测试组已经为网络边界情况场景累积了许多测试。该测试组中的一些测试将被修改为运行两次,第二次使用 -Djdk.net.usePlainDatagramSocketImpl
来确保在 JDK 同时包含两种实现期间,旧实现不会退化。如有需要,将添加新测试以扩展代码覆盖率并增加对新实现的信心。
将尽一切努力宣传该提案,并鼓励使用 DatagramSocket
和 MulticastSocket
的开发者使用在 jdk.java.net 上发布的早期访问构建版本来测试他们的代码。
jdk/jdk
仓库中的微基准测试包括 DatagramChannel
的基准测试。如果缺少针对数据报套接字的类似基准测试,则将创建它们;如果它们已经存在,则将更新它们,以便轻松比较新旧实现。
风险与假设
本提案的主要风险在于,现有代码可能依赖于在旧实现和新实现行为不同的边缘情况下未指定的行为。为了最大限度地降低这种风险,已经在 JDK 14 和 JDK 15 中进行了一些准备工作,以明确 DatagramSocket
和 MulticastSocket
的规范,并尽量减少这些类与 DatagramChannel::socket
适配器之间的行为差异。然而,以下列出的一些小差异可能仍然存在。这些差异可能在边缘情况下被观察到,但对于绝大多数 API 用户来说应该是透明的。到目前为止,我们已经识别出的差异列在这里;除了前两个差异外,其他差异都可以通过运行带有 -Djdk.net.usePlainDatagramSocketImpl
或 -Djdk.net.usePlainDatagramSocketImpl=true
标志的程序来减轻。
依赖于
DatagramSocket
和MulticastSocket
实例进行同步的自定义 API 或子类可能需要重新考虑,因为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 套接字错误代码映射为仅英文的消息,而新的基于 NIO 的实现则使用系统消息。
其他可观察到的行为差异:
通过其公共构造函数创建的
DatagramSocket
支持设置用于发送多播数据报的选项。新实现在所有平台上都允许您在DatagramSocket
的基本实例上配置多播套接字选项。旧实现仍在 Windows 上使用双栈实现,该实现不支持在DatagramSocket
基本实例上设置多播套接字选项。在这种情况下,如果需要配置此类选项,则必须使用MulticastSocket
的实例。新实现通过委托给 NIO 实现修复了一些问题,如 8165653,这些问题在 NIO 实现中不存在。
除了行为差异之外,新实现在某些工作负载下的性能可能与旧实现有所不同。本 JEP 将努力提供一些性能基准来评估这种差异。
依赖项
- 替换
DatagramSocket
和MulticastSocket
的底层实现是 Project Loom 的先决条件。