JEP 353:重新实现旧版套接字 API
概括
java.net.Socket
将和API使用的底层实现替换java.net.ServerSocket
为更简单、更现代、易于维护和调试的实现。新的实现将很容易适应用户模式线程(又名光纤),目前正在Loom 项目中进行探索。
动机
java.net.Socket
和APIjava.net.ServerSocket
及其底层实现可以追溯到 JDK 1.0。该实现是遗留 Java 和 C 代码的混合,维护和调试非常困难。该实现使用线程堆栈作为 I/O 缓冲区,这种方法需要多次增加默认线程堆栈大小。该实现使用本机数据结构来支持异步关闭,这是多年来微妙的可靠性和移植问题的根源。该实现还存在一些并发问题,需要进行彻底修改才能正确解决。在未来世界的光纤世界中,光纤将在本机方法中停放而不是阻塞线程,当前的实现不适合目的。
描述
java.net.Socket
和APIjava.net.ServerSocket
将所有套接字操作委托给java.net.SocketImpl
,这是一种自 JDK 1.0 以来就存在的服务提供者接口 (SPI) 机制。内置实现称为“普通”实现,由非公开的PlainSocketImpl
支持类SocketInputStream
和SocketOutputStream
.PlainSocketImpl
由另外两个 JDK 内部实现扩展,支持通过 SOCKS 和 HTTP 代理服务器的连接。默认情况下, aSocket
和ServerSocket
是使用基于 SOCKS 的SocketImpl
.就 而言ServerSocket
,SOCKS 实现的使用是一个奇怪的现象,可以追溯到 JDK 1.4 中对代理服务器连接的实验性支持(现已删除)。
新的实现NioSocketImpl
是 的直接替代品PlainSocketImpl
。它的开发是为了易于维护和调试。它与 New I/O (NIO) 实现共享相同的 JDK 内部基础结构,因此不需要自己的本机代码。它与现有的缓冲区缓存机制集成,因此不需要使用线程堆栈进行 I/O。它使用java.util.concurrent
锁而不是synchronized
方法,以便将来可以与光纤很好地配合。在 JDK 11 中,NIOSocketChannel
和其他SelectableChannel
实现大多是出于相同的目标而重新实现的。
以下是关于新实施的几点:
-
SocketImpl
是一种遗留的 SPI 机制,并且非常不明确。新的实现尝试通过模拟未指定的行为和适用的异常来与旧的实现兼容。下面的风险和假设部分详细介绍了新旧实现之间的行为差异。 -
connect
使用超时( 、accept
、 )的套接字操作read
是通过将套接字更改为非阻塞模式并轮询套接字来实现的。 -
该
java.lang.ref.Cleaner
机制用于在SocketImpl
垃圾收集并且套接字尚未显式关闭时关闭套接字。 -
连接重置处理的实现方式与旧实现相同,因此连接重置后尝试读取将始终失败。
ServerSocket
默认情况下修改为使用NioSocketImpl
(或PlainSocketImpl
)。它不再使用 SOCKS 实现。
SocketImpl
支持 SOCKS 和 HTTP 代理服务器的实现被修改为委托,以便它们可以与旧的和新的实现一起使用。
Java Flight Recorder 中对套接字 I/O 的检测支持已修改为独立于 Java Flight Recorder,SocketImpl
以便在使用新的、旧的或自定义实现运行时可以记录套接字 I/O 事件。
为了降低二十多年后切换实现的风险,旧的实现不会被删除。旧的实现将保留在 JDK 中,并且将引入系统属性来配置 JDK 以使用旧的实现。用于切换到旧实现的 JDK 特定系统属性是jdk.net.usePlainSocketImpl
。如果在启动时设置或设置为 value true
,则将使用旧的实现。某些未来版本将删除PlainSocketImpl
系统属性。
DatagramSocketImpl
此 JEP目前不建议提供替代实现(是委托DatagramSocketImpl
实例的底层实现)。java.net.DatagramSocket
内置默认实现 ( PlainDatagramSocketImpl
) 是维护(和移植)负担,并且可能是另一个 JEP 的主题。
测试
存储库中的现有测试jdk/jdk
将用于测试新的实现。测试组jdk_net
多年来积累了许多针对网络极端场景的测试。该测试组中的一些测试将被修改为运行两次,第二次是为了-Djdk.net.usePlainSocketImpl
确保旧的实现在 JDK 包含这两个实现期间不会发生位腐烂。
如今,许多代码直接或间接使用使用 中定义的 API 的库,java.nio.channels
而不是java.net.Socket
和java.net.ServerSocket
API。我们将尽一切努力提高对该提案的认识,并鼓励使用代码的开发人员使用jdk.java.net或其他地方发布的早期访问版本来测试他们的代码Socket
。ServerSocket
存储库中的微基准jdk/jdk
包括套接字读/写和流的基准。这些基准已得到改进,可以轻松比较新旧实施。就目前情况而言,在套接字读/写测试中,新实现与旧实现大致相同或好 1-3%。
风险和假设
该提案的主要风险是,现有代码依赖于新旧实现行为不同的极端情况下的未指定行为。此处列出了迄今为止已发现的差异;除了前两个之外的所有问题都可以通过运行来缓解-Djdk.net.usePlainSocketImpl
。
-
和方法返回的
InputStream
和分别扩展了和。有可能但不太可能存在依赖于此的现有代码。OutputStream``PlainSocketImpl``getInputStream()``getOutputStream()``java.io.FileInputStream``java.io.FileOutputStream
-
ServerSocket
使用自定义的ASocketImpl
不能接受返回Socket
带有平台的连接SocketImpl
。同样,ServerSocket
使用该平台的SocketImpl
a 不能接受返回Socket
带有自定义SocketImpl
. -
InputStream
旧实现返回的 and测试OutputStream
流的 EOF 并在其他检查之前返回 -1。新的实现null
在检查流是否位于 EOF 之前进行边界检查。有可能,但不太可能,存在脆弱的代码,由于检查的顺序而出错。 -
关闭
Socket
接收队列中未读字节将正常关闭底层套接字。在 Microsoft Windows 以外的平台上,与旧实现相同的情况将导致中止/硬关闭。 -
Oracle Solaris 特定:Oracle Solaris 与其他平台的不同之处在于它向应用程序报告“连接重置”的方式。例如,当出现网络错误时,对
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
类以获取 I/O 事件。新实现不使用这些类。