http://openjdk.java.net/jeps/353
JEP 353: Reimplement the Legacy Socket API
Owner Alan Bateman Type Feature Scope JDK Status Proposed to Target Release 13 Component core-libs / java.net Discussion net dash dev at openjdk dot java dot net Effort S Reviewed by Brian Goetz, Chris Hegarty, Michael McMahon Endorsed by Brian Goetz Created 2019/02/06 13:49 Updated 2019/05/16 19:55 Issue 8218559
Summary
java.net.Socket
およびjava.net.ServerSocket
APIで使われる基底実装のリプレース。メンテナンスとデバッグがしやすいシンプルで近代的な実装にします。新規実装は、ユーザモードスレッドいわゆるfibersを使いやすいようにします。Project Loomを参照してください。
Motivation
java.net.Socket
とjava.net.ServerSocket
APiとその基底実装はJDK 1.0の時代から存在します。この実装はレガシーJavaとCのミックスでメンテナンスとデバッグが極めて困難です。I/Oバッファーにスレッドスタックを使用しており、この方法ではデフォルトのスレッドスタックサイズを数回増やす必要がありました。非同期closeにネイティブデータ構造を使用しており、厄介な信頼性と移植性の問題が長年続いています。また、コンカンレンシーの問題もあり適切に対処するには全体的な改修が必要です。ネイティブメソッドでスレッドをブロックする代わりにfiberを将来的に使用する事を想定すると、現行実装ではフィットしません。
Description
java.net.Socket
とjava.net.ServerSocket
APIはすべてのソケット操作をjava.net.SocketImpl
にデリゲートし、こうしたService Provider Interface (SPI)はJDK 1.0から存在しています。これの組み込み実装は“plain”実装と呼ばれており、非publicのPlainSocketImpl
とそのサポートクラスSocketInputStream
とSocketOutputStream
がそれです。PlainSocketImpl
は2つのJDK内部実装が拡張しており、SOCKSとHTTPプロキシサーバ経由のコネクションを行います。デフォルトでは、Socket
とServerSocket
はSOCKSベースのSocketImpl
で生成(場合により遅延生成)します。ServerSocket
の場合、SOCKS実装の使用は奇妙な事であり、これはJDK 1.4のプロキシーサーバコネクションの実験的サポート(と削除)にさかのぼります*1。
新実装NioSocketImpl
はPlainSocketImpl
のdrop-in replacementです。メンテナンスとデバッグをしやすくするために開発します。New I/O (NIO)などJDK内部機能を使用し、ネイティブコードを無くします。既存のバッファキャッシュ機能と統合し、I/Oにスレッドスタックを使う必要性を無くします。synchronized
メソッドではなくjava.util.concurrent
のロックを使用し、将来的にfibersを使用できるように備えます。JDK 11のNIO SocketChannel
とその他のSelectableChannel
実装の再実装は同じ目標の下に実施しました。
以下は新規実装のポイントです。
SocketImpl
はレガシーSPIで極めてunder-specifiedです。新規実装は旧実装との互換性維持を、unspecifiedな振る舞いと該当する例外(exceptions where applicable)のエミュレートで行います。以下のRisks and Assumptionsセクションで新旧実装間の振る舞いの差の詳細を説明します。- タイムアウトを使うSocket操作(
connect
,accept
,read
)は、ソケットをノンブロッキングモードとソケットポーリングに変更して実装します。 java.lang.ref.Cleaner
はSocketImpl
がGC時にソケットをクローズするのに使用し、ソケットの明示的クローズは行っていません。- コネクションリセットハンドリングは旧実装と同様な方法で実装し、コネクションリセット後にreadしようとすると一貫して失敗します。
ServerSocket
はデフォルトではNioSocketImpl
(もしくはPlainSocketImpl
)を使うよう修正します。SOCKS実装は使いません。
SOCKSとHTTPプロキシサーバのSocketImpl
実装はデリゲートするよう修正し、新旧実装どちらでも動かせるようにします。
Java Flight RecorderのソケットI/O用のinstrumentation supportはSocketImpl
と独立するよう修正し、新・旧・カスタム実装のいずれの実行時にもソケットI/Oイベントを記録可能にします。
20年後に実装を切り替える際のリスク軽減のため、旧実装は削除しません。旧実装はJDKに残して、旧実装を使うためのJDKシステムプロパティを導入します。旧実装に切り替えるJDKのシステムプロパティはjdk.net.usePlainSocketImpl
です。これを設定、ないしtrue
に設定すると、起動時に旧実装を使用するようになります。将来リリースでPlainSocketImpl
とシステムプロパティを削除する予定です。
本JEPの時点ではDatagramSocketImpl
の代替実装は提供しません(DatagramSocketImpl
はjava.net.DatagramSocket
がデリゲートする基底実装です)。組み込みデフォルト実装(PlainDatagramSocketImpl
)はメンテナンス(と移植)の負荷が高く、別のJEPで扱う予定です。
Testing
jdk/jdk
リポジトリの既存テストを新規実装に使用します。jdk_net
テストグループでは長年にわたりネットワーキングにおける多数のコーナーケースの蓄積があります。テストグループのいくつかのテストは2回実行するよう修正し、2回目では-Djdk.net.usePlainSocketImpl
でJDKが新旧実装を両方含む場合でも旧実装がbit-rot*2しないことを確認します。
今日では、java.net.Socket
とjava.net.ServerSocket
でなくjava.nio.channels
を使うライブラリを直接・間接に使用するコードが多くあります。本提案の周知を行い、Socket
とServerSocket
を使っているコードをearly-accessビルドでテストする事を開発者に奨励するような活動を行う予定です。early-accessビルドはjdk.java.netかどこかで公開します。
jdk/jdk
リポジトリのマイクロベンチマークにはソケットのread/writeとストリーミング用のテストがあります。これらベンチマークは新旧実装間の比較を簡単に出来るよう改良が加えられてきました。現状、ソケットread/writeテストにおいて、新規実装は旧実装に比べて同じか1-3%良くなっています。
Risks and Assumptions
本提案の第一のリスクは、新旧実装で異なる振る舞いをするコーナーケースにおいてunspecified behaviorに依存する既存コードです。現在認識してえる相違のリストは以下の通りで、最初の2つ以外は-Djdk.net.usePlainSocketImpl
により軽減できます。
PlainSocketImpl
のgetInputStream()
とgetOutputStream()
が返すInputStream
とOutputStream
は、それぞれjava.io.FileInputStream
とjava.io.FileOutputStream
を拡張しています。これに依存する既存コードは、理論的にはありえますが、可能性としては低いです。- カスタムの
SocketImpl
を使用するServerSocket
は、プラットフォームのSocketImpl
を使うSocket
を返すコネクションは使えません。同様に、プラットフォームのSocketImpl
を使うServerSocket
は、カスタムのSocketImpl
を使うSocket
を返すコネクションを使えません。 - 旧実装が返す
InputStream
とOutputStream
はストリームのEOFをチェックして他のチェック前に-1を返します。新規実装はストリームEOFチェック前にnull
と境界チェックをします。チェック順序に依存する壊れやすい実装は、理論的にはありえますが、可能性としては低いです。 - Java SE 9で
Socket
とServerSocket
に導入されたsetOption(SocketOption<T>, T value)
は、ソケットオプションの値が非validな場合にIllegalArgumentException
をスローします(例:ソケットバッファサイズに負の値を指定)。旧実装は間違った例外をスローする場合があります。新規実装は期待通りの例外をスローするものの、既存コードが現在とは異なる方法で失敗する可能性があります。 - Oracle Solaris specific: Oracle Solarisはアプリケーションに"connection reset"を伝える方法が他のプラットフォームと異なります。例えば、
setsockopt
やioctl
はネットワークエラーで失敗する可能性があります。/etc/system
にxnet_skip_checks
を設定することでこの振る舞いを無効化できます(echo "xnet_skip_checks/W 1" | mdb -kw
)。The old implementation handles the case where ioctl(FIOREAD) fails so that attempts to read after available fails with a “connection reset” will fail consistently. This is fragile and unmaintainable, the new implementation does not attempt to emulate this behavior. - Oracle Solaris specific: Oracle Solarisは接続後にTCPソケットにIPV6_TLCASSソケットオプションを変更出来ません。旧実装は
setTrafficClass
の指定値をキャッシュすることでこれをマスクしています。 java.net
パッケージにはSocketException
のサブクラスが多数存在します。新規実装は旧実装と同じSocketException
をスローしますが、そうでないものもあります。また、例外メッセージが異なるケースも多数あります。例えばMicrosoft Windowsでは、旧実装はWindows Socketエラーコードを英語オンリーのメッセージに変換していましたが、新規実装はシステムメッセージを使用します。
振る舞いの相違とは別に、新規実装のパフォーマンスは特定のワークロード実行時に旧実装と異なる可能性があります。旧実装では、ServerSocket
のaccept
を呼ぶ複数のスレッドがカーネルにキューします。新規実装では、単一スレッドがaccept
システムコールをブロックし、それ以外はjava.util.concurrent
のロックを得るためにqueue waitingします。パフォーマンス特性はこれ以外の場合にも起こりえます。
最後に、instrumentation agentsやツールでI/Oイベント取得に非publicのjava.net.SocketInputStream
とjava.net.SocketOutputStream
クラスを呼ぶものがあります。これらのクラスは新規実装では利用できません。