原文はこちら。
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/bar
や C:\foo\bar
)で指定されています。
Why use them?
ローカルIPCでのUnixドメインソケットの利用には多数の利点があります。
Performance
(127.0.0.1
もしくは::1
に接続する)ループバックTCP/IPソケットではなくUnixドメインソケットを使うと、TCP/IPスタックをバイパスするため、結果としてレイテンシやCPU利用率が改善されます。

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
この例では、以下のシンプルなClient
とServer
を、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が既にインストール済みの既存イメージと、Client
とServer
がすでにホスト上で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特有の機能は現在サポートされていませんが、将来的には考慮される可能性があります。
- Linuxの抽象パス
抽象パスはファイルシステムオブジェクトとリンクしていないため、所有するソケットとは無関係に独立して存在しないという点で TCP/IP のポート番号に似ています。 - データグラムのサポート
java.net.Socket
とjava.net.ServerSocket
のサポート
レガシーのソケットネットワーキングクラスは、アドレス指定にjava.net.InetAddress
を固定的に使用しているため、主にTCP/IPに縛られています。- 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