JEP-380: Unix domain socket channels

原文はこちら。
The original article was written by Michael McMahon (Software Engineer, Oracle).
https://inside.java/2021/02/03/jep380-unix-domain-sockets-channels/

SocketChannelおよびServerSocketChannel APIは、TCP/IPソケットへのブロッキングアクセスと、多重化されたノンブロッキングアクセスを提供します。Java 16ではこれらのクラスが拡張され、同じシステム内の内部IPCのためのUnixドメイン(AF_UNIX)ソケットをサポートします。この記事では、サポートが追加されたこの機能の使い方を説明するとともに、同じシステム上の異なるDockerコンテナ内のプロセス間の通信など、他のユースケースについても説明します。

JEP 380: Unix-Domain Socket Channels
https://openjdk.java.net/jeps/380
Class SocketChannel
https://download.java.net/java/early_access/jdk16/docs/api/java.base/java/nio/channels/SocketChannel.html
Class ServerSocketChannel
https://download.java.net/java/early_access/jdk16/docs/api/java.base/java/nio/channels/ServerSocketChannel.html

What are Unix domain sockets

TCP/IPソケットは、IP アドレスとポート番号でアドレスを指定し、インターネットやプライベートネットワーク上でのネットワーク通信に使用されます。一方Unixドメインソケットは、同じ物理ホスト上でのプロセス間通信にのみ使用されます。これは何十年も前からUnixオペレーティングシステムの機能でしたが、Microsoft Windowsに追加されたのは最近のことです。そのため、もはやUnixに限定されたものではありません。Unixドメインソケットはファイルシステムのパス名(/foo/barC:\foo\bar)で指定されています。

Why use them?

ローカルIPCでのUnixドメインソケットの利用には多数の利点があります。

Performance

127.0.0.1もしくは::1に接続する)ループバックTCP/IPソケットではなくUnixドメインソケットを使うと、TCP/IPスタックをバイパスするため、結果としてレイテンシやCPU利用率が改善されます。

book cover

Security

第一に、サービスへのローカルアクセスは必要だが、リモートネットワークアクセスは必要ない場合、サービスをローカルパス名として公開することで、リモートクライアントからのサービスに対する予期せぬアクセスを避けることができます。

第二に、Unixドメインソケットはファイルシステムオブジェクトでアドレスが指定されるため、標準的な Unix(および Windows)ファイルシステムのアクセス制御を適用して、必要に応じて特定のユーザやグループによるサービスへのアクセスを制限できます。

Convenience

Dockerコンテナのような環境では、TCP/IPソケットを使って異なるコンテナ内のプロセス間の通信リンクを設定するのが面倒な場合があります。後述の例では、2つのDockerコンテナ間で共有ボリューム内でUnixドメインソケットを使って通信する方法を示しています。

Compatibility

後述するように、TCP/IPソケットとUnixドメインソケットの間の明らかなアドレッシングの違いを考慮すれば、互換性のある動作が期待できます。特に、一度チャネルが作成され、アドレスにバインドされると、アドレッシングに依存しない既存のコードは、Unixドメインソケットでも TCP/IP ソケットでも変更なしに動作するはずです。そのため、例えばSelectorはコードを変更せずにどちらのタイプのソケットも扱うことができます。

Class Selector
https://download.java.net/java/early_access/jdk16/docs/api/java.base/java/nio/channels/Selector.html

How to use Unix domain sockets

Java 16より前の既存のAPIは引き続きTCP/IPソケットを作成します。従って、以下で説明する新しいAPIを使ってUnixドメインソケットを作成、バインドする必要があります。

まず、Unixドメインのクライアントまたはサーバソケットを作成するには、以下に示すようにプロトコルファミリーを指定します。

    // Create a Unix domain server
    ServerSocketChannel server = ServerSocketChannel.open(StandardProtocolFamily.UNIX);

    // Or create a Unix domain client socket
    SocketChannel client = SocketChannel.open(StandardProtocolFamily.UNIX);

ソケットをアドレスにバインドする場合に2つ目の違いが現れます。これをサポートするために、新たに定義されたUnixDomainSocketAddressという新しい型を既存のbindメソッドのいずれかに渡す必要があります。

Class UnixDomainSocketAddress
https://download.java.net/java/early_access/jdk16/docs/api/java.base/java/net/UnixDomainSocketAddress.html

以下のように、2個あるうちのいずれかのファクトリーメソッドを使ってUnixDomainSocketAddressインスタンスを作成します。

    // Create an address directly from the string name and bind it to a socket
    var socketAddress = UnixDomainSocketAddress.of("/foo/bar.socket");
    socket.bind(socketAddress);

    // Create an address from a Path and bind it to a socket
    var socketAddress1 = UnixDomainSocketAddress.of(Path.of("/foo", "bar.socket"));
    socket1.bind(socketAddress1);

Java 16より前に定義されている既存のメソッドが常にTCP/IPソケットを生成するという上記のルールの例外として、SocketAddressを受け取り、指定されたアドレスに接続されたクライアントソケットを返す便利なopenメソッドがあります。この場合、以下に示すように、与えられたアドレスオブジェクトからプロトコルファミリーを推論します。

Class SocketAddress
https://download.java.net/java/early_access/jdk16/docs/api/java.base/java/net/SocketAddress.html

    var inetAddr = new InetSocketAddress("host", 80);
    var channel1 = SocketChannel.open(inetAddr);

    var unixAddr = UnixDomainSocketAddress.of("/foo/bar.socket");
    var channel2 = SocketChannel.open(unixAddr);

上記の例では、channel1ではTCP/IPプロトコルファミリーであるINETもしくはINET6、対してchannel2ではUNIXプロトコルファミリーを使用しています。

INET (IPv4)
https://download.java.net/java/early_access/jdk16/docs/api/java.base/java/net/StandardProtocolFamily.html#INET
INET6 (IPv6)
https://download.java.net/java/early_access/jdk16/docs/api/java.base/java/net/StandardProtocolFamily.html#INET6
UNIX
https://download.java.net/java/early_access/jdk16/docs/api/java.base/java/net/StandardProtocolFamily.html#UNIX

両ソケットの間でもう一つAPIの違いがあります。それは、サポート対象のオプションのセットが異なる、という点です。予想されている通り、TCP/IP固有のオプションはUnixドメインソケットでサポートされません。

でもそれだけです。これらが両ソケットにおけるAPIの違いです。ただし、Unixドメインソケットのより具体的な側面について、以下を読んで知っておく必要があります。

A deeper look at Unix domain sockets

前章で示したように、アドレッシングモデルには共通点がありますが、注意すべき重要な違いもあります。

Socket files exist independent of their sockets (ソケットファイルはソケットと独立して存在する)

TCP/IPのポート番号はソケットと密接に結びついています。特に、TCP/IPソケットをクローズすると、そのポート番号は(最終的には)再利用のために解放されることが想定されますが、Unixドメインソケットの場合はそうではありません。

バインドされたUnixドメインソケットをクローズすると、ファイルを明示的に削除するまで、そのファイルシステムノードが残ります。同じ名前でソケットファイル(または他の種類のファイル)が存在する間は、後に続くソケットは同じ名前でバインドできません。このため、サーバをシャットダウンした後の後始末の際には、ソケットファイルを確実に削除することをお勧めします。

Client sockets do not need to be bound to a specific name(クライアントソケットを特定の名前にバインドする必要はない)

TCP/IPでは、クライアントソケットを明示的にバインドしない場合、OSがローカルポート番号を暗黙のうちに選択しますが、Unixドメインソケットの場合は少々異なります。このような場合には、ソケットに名前を付けずにunnamed(無名)にします。無名ソケットには対応するファイルシステムノードがないため、ソケットが閉じた後のファイル削除を心配する必要がありません。

明示的にクライアントソケットを名前にバインドすることももちろん可能ですが、名前にバインドする場合、ソケットをクローズした後にソケットファイルを削除する必要があります。

Server sockets are always bound to a name(サーバーソケットは常に名前にバインドされる)

明らかに、クライアントがアクセスできるようにするためには、サーバは常にバインドされていなければなりません。しかし、名前はwell knownでなくてもかまいません。ですから、TCP/IP ソケットを使って bind(null) をコールすると、システムは自動的にポート番号を選択しますし、その情報をクライアントに渡すための帯域外のメカニズムを使用できます。同様のメカニズムはUnixドメインのServerSocketChannelsにも存在します。bind(null)を呼び出すと、システムは自動的にシステムの一時的な場所にバインドするためのユニークなパス名を選択します。本件とその場所を変更する方法についての詳細は、APIドキュメントを参照してください。

Unix domain sockets
https://download.java.net/java/early_access/jdk16/docs/api/java.base/java/net/doc-files/net-properties.html#Unixdomain

また、どちらの種類のServerSocketChannelでも、getLocalAddressメソッドは実際にバインドされたアドレスを返すことに注意してください。

Unix domain socket addresses are limited in length(Unixドメインソケットアドレスは長さに制限がある)

すべてのオペレーティングシステムでは、Unixドメインのソケットアドレスの長さに厳しい制限を課しています。この値はプラットフォームによって異なり、一般的には約100バイトです。これは、特に深いディレクトリパス名が使用されている場合に問題になることがあります。

この問題は、相対パス名を使用するなど、いくつかの方法で回避できます。実際のファイルは任意の深いディレクトリに配置でき、サーバとそのクライアントが同じ作業ディレクトリを使用する限り、相対パス名の長さは100バイト以下になるように調整できます。

また、システムのテンポラリディレクトリが特に長い名前を持つ場合には、サーバの自動バインディング(bind(null))で問題が発生することもあります。上述の通り、このディレクトリの選択をオーバーライドするためのプラットフォーム固有のメカニズムがあります。

Obtaining remote user credentials

JDK 16では、(プラットフォーム固有の)ソケットオプション(SO_PEERCRED)も追加されています。これはUnixシステムで利用でき、接続されたピアのユーザー名とグループ名をカプセル化したUnixDomainPrincipalを返します。

SO_PEERCRED
https://download.java.net/java/early_access/jdk16/docs/api/jdk.net/jdk/net/ExtendedSocketOptions.html#SO_PEERCRED
Record Class UnixDomainPrincipal
https://download.java.net/java/early_access/jdk16/docs/api/jdk.net/jdk/net/UnixDomainPrincipal.html

Using Unix domain sockets to communicate between two Docker containers

この例では、以下のシンプルなClientServerを、JDK 16が既にインストールされている2つの異なるAlpine Linux Dockerコンテナにインストールします。ホスト上の共有ボリューム(myvol)を作成し、両方のコンテナで(/mntに)この共有ボリュームをマウントします。一方のコンテナで動作しているサーバーは/mnt/serverにバインドされたServerSocketChannelを作成し、もう一方のコンテナで動作しているクライアントは同じアドレスに接続しています。

    import java.net.*;
    import java.nio.*;
    import java.nio.channels.*;
    import java.nio.file.*;
    import java.io.*;

    import static java.net.StandardProtocolFamily.*;

    public class Server {
        public static void main(String[] args) throws Exception {
            var address = UnixDomainSocketAddress.of("/mnt/server");
            try (var serverChannel = ServerSocketChannel.open(UNIX)) {
                serverChannel.bind(address);
                try (var clientChannel = serverChannel.accept()) {
                    ByteBuffer buf = ByteBuffer.allocate(64);
                    clientChannel.read(buf);
                    buf.flip();
                    System.out.printf("Read %d bytes\n", buf.remaining());
                }
            } finally {
                Files.deleteIfExists(address.getPath());
            }
        }
    }

    // assume same imports
    public class Client {
        public static void main(String[] args) throws Exception {
            var address = UnixDomainSocketAddress.of("/mnt/server");
            try (var clientChannel = SocketChannel.open(address)) {
                ByteBuffer buf = ByteBuffer.wrap("Hello world".getBytes());
                clientChannel.write(buf);
            }
        }
    }

そして最後にすべてを動作させるためのDockerコマンドです。これはJDK 16が既にインストール済みの既存イメージと、ClientServerがすでにホスト上でjavacでコンパイルされている前提です。

    // creates a shareable volume called myvol
    docker volume create myvol

    // In one window run an image mounting the shared volume
    // Assume alpine_jdk_16 is a local image with jdk 16 installed
    docker run --mount 'type=volume,destination=/mnt,src=myvol' -it alpine_jdk_16 sh

    // In 2nd window run same
    docker run --mount 'type=volume,destination=/mnt,src=myvol' -it alpine_jdk_16 sh

    // In 3rd window: get container ids (substitute ids below as appropriate)
    docker ps

    docker cp Client.class 3c7e76e9f1a2:/root

    docker cp Server.class 687de1ed186c:/root

    // Go back and run Server in 687de1ed186c and Client in 3c7e76e9f1a2

Inherited channels

継承されたチャネルメカニズムはJava 1.5から存在し、Java仮想マシンがLinuxのinetdやmacOSのlaunchdのようなメカニズムから起動されると、常にTCP/IPソケットを返すことができました。

inheritedChannel
https://download.java.net/java/early_access/jdk16/docs/api/java.base/java/nio/channels/spi/SelectorProvider.html#inheritedChannel()

このたび、基礎となる起動フレームワークがサポートしていれば、このメカニズムでUnixドメインソケットをサポートするようになりました。

Limitations

JEP 380では主なサポート対象プラットフォームに共通する機能に注力しました。以下のUnix/Linux特有の機能は現在サポートされていませんが、将来的には考慮される可能性があります。

  1. Linuxの抽象パス
    抽象パスはファイルシステムオブジェクトとリンクしていないため、所有するソケットとは無関係に独立して存在しないという点で TCP/IP のポート番号に似ています。
  2. データグラムのサポート
  3. java.net.Socket と java.net.ServerSocket のサポート
    レガシーのソケットネットワーキングクラスは、アドレス指定に java.net.InetAddress を固定的に使用しているため、主にTCP/IPに縛られています。
  4. UnixドメインのSocketChannelを使った送信チャネル
    Unixドメインソケットは単一のシステムに限定されているため、接続されたピア間でデータ以外のオブジェクトを送信するために使用できます。原則として、この方法でファイルディスクリプタで表されるオブジェクトをプロセス間で送信できます。実際には、この機能をNIOチャネルオブジェクトに限定する可能性が最も高いでしょう。

Conclusion

Java 16における、SocketChannelとServerSocketChannelのUnixドメインソケットの簡単なご紹介でした。いつも通り、完全な仕様はNIO channel APIドキュメントをチェックしてください。プラットフォーム固有のソケットオプションはjdk.netで定義されています。

Package java.nio.channels
https://download.java.net/java/early_access/jdk16/docs/api/java.base/java/nio/channels/package-summary.html
Class ExtendedSocketOptions
https://download.java.net/java/early_access/jdk16/docs/api/jdk.net/jdk/net/ExtendedSocketOptions.html

コメントを残す

以下に詳細を記入するか、アイコンをクリックしてログインしてください。

WordPress.com ロゴ

WordPress.com アカウントを使ってコメントしています。 ログアウト /  変更 )

Twitter 画像

Twitter アカウントを使ってコメントしています。 ログアウト /  変更 )

Facebook の写真

Facebook アカウントを使ってコメントしています。 ログアウト /  変更 )

%s と連携中