JEP 373:重新实现旧版 DatagramSocket API
概括
java.net.DatagramSocket
将和API的底层实现替换java.net.MulticastSocket
为更简单、更现代、易于维护和调试的实现。新的实现将很容易适应虚拟线程,目前正在Loom 项目中进行探索。这是JEP 353 的后续版本,它已经重新实现了旧版 Socket API。
动机
java.net.DatagramSocket
和API的 代码库java.net.MulticastSocket
及其底层实现陈旧且脆弱:
-
这些实现可以追溯到 JDK 1.0。它们是遗留 Java 和 C 代码的混合体,难以维护和调试。
-
它的实施
MulticastSocket
尤其存在问题,因为它可以追溯到 IPv6 仍处于开发阶段的时期。许多底层本机实现试图以难以维护的方式协调 IPv4 和 IPv6。 -
该实现还存在一些并发问题(例如,异步关闭),需要进行彻底修改才能正确解决。
此外,在虚拟线程在系统调用中驻留而不是阻止底层内核线程的情况下,当前的实现不适合目的。随着基于数据报的传输再次受到关注(例如 QUIC),需要更简单且更可维护的实现。
描述
目前,DatagramSocket
和MulticastSocket
类将所有套接字调用委托给一个java.net.DatagramSocketImpl
实现,该实现存在不同的特定于平台的具体实现:PlainDatagramSocketImpl
在 Unix 平台上,和TwoStackPlainDatagramSocketImpl
在DualPlainDatagramSocketImpl
Windows 平台上。这个抽象DatagramSocketImpl
类可以追溯到 JDK 1.1,它的规范非常不明确,并且包含几个过时的方法,这些方法阻碍了提供基于 NIO 的此类的实现(请参阅下面讨论的替代方案)。
该 JEP 建议在内部包装另一个实例,将所有调用直接委托给该实例,而不是为 的实现提供直接替代DatagramSocketImpl
,类似于JEP 353中为所做的事情。 包装的实例要么是从 NIO (新实现)创建的套接字适配器,要么是遗留类的克隆,然后委托给遗留实现(为了实现向后兼容性开关)。如果由应用程序安装,则选择旧的遗留实现。否则,默认选择并使用新的实现。SocketImpl``DatagramSocket``DatagramSocket``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
and sun.nio.ch.DatagramSocketAdaptor
。升级sun.nio.ch.DatagramSocketAdaptor
扩展java.net.MulticastSocket
。
这种方法表明提供DatagramSocketImpl
基于DatagramChannel
.测试通过了,但也凸显了一些局限性:
-
安全检查执行了两次,一次在 中
DatagramSocket
,另一次在DatagramChannel
(或其套接字适配器)中。有一些方法可以避免双重安全检查,但它们会很麻烦。 -
在该级别实现的连接模拟
DatagramSocket
也受到阻碍,因为我们不想使用基于 NIO 的实现来执行此模拟。 -
与上面提出的解决方案一样,与下面的第二个替代方案相比,此替代方案的主要优点是不需要新的本机代码,因为每个调用都可以委托给
DatagramChannel
. -
在评估此替代方案时,我们很快就发现,在级别上覆盖方法
DatagramSocket
(而不是在DatagramSocketImpl
级别上)会更简单、更直接,这导致了本 JEP 中提出的解决方案。
替代方案2
在调用低级原语的包DatagramSocketImpl
中创建 的实现。这允许实现直接访问较低级别的 NIO 原语,而不是依赖于.这在某种程度上类似于重新实现和JEP 353中所做的事情。sun.nio.ch``sun.nio.ch.Net``DatagramChannel``Socket``ServerSocket
-
与第一个替代方案相比,此替代方案的主要优点是它避免了双重安全检查,因为该实现可以直接访问较低级别的 NIO 原语。
-
然而,新的实现必须复制已经实现的重要状态和锁管理
DatagramChannel
。 -
它还需要添加新的本机代码来匹配
DatagramSocketImpl
界面。 -
因此,本 JEP 中提出的解决方案显得更简单、风险更小且更易于维护。
测试
存储库中的现有测试jdk/jdk
将用于测试新的实现。为了确保平稳过渡,新的实现应该通过 tier2(jdk_net
和jdk_nio
)回归测试套件和 JCK for java_net/api
.测试组jdk_net
多年来积累了许多针对网络极端场景的测试。该测试组中的一些测试将被修改为运行两次,第二次是为了-Djdk.net.usePlainDatagramSocketImpl
确保旧的实现在 JDK 包含这两个实现期间不会衰减。将根据需要添加新的测试,以扩大代码覆盖范围并增强对新实现的信心。
我们将尽一切努力宣传该提案,并鼓励拥有代码的开发人员使用jdk.java.net上发布的早期访问版本来测试他们的DatagramSocket
代码。MulticastSocket
存储库中的微基准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
通过其公共构造函数之一创建的A支持用于发送多播数据报的设置选项。新的实现允许您在所有平台的基本实例上配置多播套接字选项DatagramSocket
。旧的实现仍然在 Windows 上使用双堆栈实现,它不支持基本DatagramSocket
实例上的多播套接字选项。在这种情况下MulticastSocket
,如果需要配置此类选项,则必须使用的实例。 -
新的实现仅通过委托给 NIO 实现就解决了许多问题,例如8165653 ,而这些问题不存在。
除了行为差异之外,在运行某些工作负载时,新实现的性能可能与旧实现不同。该 JEP 将尽力提供一些性能基准来衡量差异。
依赖关系
- 替换 DatagramSocket 和 MulticastSocket 的底层实现是Loom 项目的先决条件。