Java 16 から利用可能な Unix ドメイン ソケット

Java 16 で [JEP 380: Unix-Domain Socket Channels](https://openjdk.java.net/jeps/380) が取り入れられました。今回はこれをご紹介します。

名前の通り、Unix で使われてきたものですが、Windows 10 April 2018 Update 以降は Windows でもサポートされています。Windows Subsystem for Linux (WSL) や Docker コンテナのように、ホスト OS 上の Unix と通信したい場面が増えたためだと思われます。

私なりにわかりやすくご紹介しようと思いますが、にわかユーザーに過ぎませんので、正確な情報や参考になる実装コードがほしい方は、ぜひ JEP 380 のオーナーさんによる [Inside Java - JEP-380: Unix domain socket channels
](https://inside.java/2021/02/03/jep380-unix-domain-sockets-channels/) ([Logico Inside の和訳](https://logico-jp.io/2021/02/05/jep-380-unix-domain-socket-channels/)) もご覧ください。


# Unix ドメイン ソケットとは

> Unix domain sockets are addressed by filesystem pathnames that look much the same as other filenames: eg. /foo/bar or C:\foo\bar.

冒頭でご紹介した [Inside Java の記事](https://inside.java/2021/02/03/jep380-unix-domain-sockets-channels/)では、上記のように説明されています。

つまり、Unix ドメイン ソケットでは、アドレスに /foo/bar や C:\foo\bar などのファイル名のようなパス名を使います。ポート番号は使いません。


## パイプと比べて

Windows では Unix ドメイン ソケットの役割を、名前付きパイプが果たしてきました。

`echo WebSocket | cat` のようにコマンドをつなぐときに使うパイプ `|` とちょっと似ています。

大きな違いは双方向で通信できることです。この例で cat から echo にデータを送り返すことはできませんが、Unix ドメイン ソケットなら双方向でやり取りできます。


## ふつうのソケットと比べて

再び [Inside Java の記事](https://inside.java/2021/02/03/jep380-unix-domain-sockets-channels/)を参考に、ローカルのプロセス間通信に Unix ドメイン ソケットを使う利点をご紹介します。

### 性能

![Comparing TCP/IP and Unix domain for local IPC between two sockets](https://inside.java/images/blog/jep380.jpg)

画像は前述の [Inside Java の記事](https://inside.java/2021/02/03/jep380-unix-domain-sockets-channels/)からの引用です。

127.0.0.1 や ::1 のようなループバックと比べ、TCP/IP の処理が不要なため、レイテンシや CPU 使用率を削減できます。

『詳解UNIXプログラミング 第3版』[^u] には具体的に、以下のように書かれていました。

> インターネットドメインソケットも同じ目的に使えますが、UNIX ドメインソケットはより効率的です。UNIX ドメインソケットはデータをコピーするだけです。処理すべきプロトコルはありませんし、付加したり削除したりするネットワークヘッダもなく、チェックサム計算も必要なく、連番を振る必要もなく、肯定応答を送る必要もありません。

言われてみれば TCP って色々と面倒なことを肩代わりしてくれているのでしたね。アプリの担当範囲ではないので関係ない感じもしますが、高速化は嬉しいです。

#### Windows 10 上の PostgreSQL 13 での実測値は TCP と同等

「[Windows 10 の PostgreSQL 13 で Unix ドメイン ソケット【TCP? なにそれおいしいの？】](https://qiita.com/hanohrs/items/0cd93915fcdd5f8f5057)」で検証した際には、性能は TCP と同じでした。通信がボトルネックになりがちな処理を選んで検証したつもりでしたが、差が出ませんでした。性能への過剰な期待は禁物です。


### 安全性

パス名をアドレスにしていれば、うっかり外部に公開してもアクセスされる恐れがありません。「おかしいなー。TCP のポートにつながらないなー。ファイアーウォールを切ってアクセスしてみよう」のように思うことは、Unix ドメイン ソケットを使っていればありません。

2点目は少し意外でした。

> Second, because Unix domain sockets are addressed by filesystem objects, this means standard Unix (and Windows) filesystem access controls can be applied to limit access to a service by specific users or groups, as required.

ファイルシステム オブジェクトとしてアクセスされるので、アクセス制御リストにより特定のユーザーやグループにのみアクセスさせることができるそうです。まるでユーザーやグループで制御できるファイアーウォールですね。

### 利便性

Docker などで TCP/IP で通信しようとしてハマらなくて済むそうです。私はめったに Docker を使わないので、よくわかりません。

### 互換性

アドレス形式は全く異なりますが、基本的には TCP/IP のソケットのように扱えるので、両方をサポートしてもあまり負担になりません。



# Java 16 で何ができるようになったの？

UNIX ドメイン ソケット自体の利点は掴んだところで、今度は解説ブログではなく [JEP 380: Unix-Domain Socket Channels](https://openjdk.java.net/jeps/380) 本体から、概要と目標をご紹介します。


## 概要

> Add Unix-domain (AF_UNIX) socket support to the [socket channel](https://docs.oracle.com/en/java/javase/15/docs/api/java.base/java/nio/channels/SocketChannel.html) and [server-socket channel](https://docs.oracle.com/en/java/javase/15/docs/api/java.base/java/nio/channels/ServerSocketChannel.html) APIs in the java.nio.channels package. Extend the [inherited channel mechanism](https://docs.oracle.com/en/java/javase/15/docs/api/java.base/java/nio/channels/spi/SelectorProvider.html#inheritedChannel()) to support Unix-domain socket channels and server socket channels.

java.nio.channels パッケージの [SocketChannel](https://docs.oracle.com/en/java/javase/16/docs/api/java.base/java/nio/channels/SocketChannel.html) に Unix ドメイン (AF_UNIX) のソケットが追加され、[ServerSocketChannel](https://docs.oracle.com/en/java/javase/16/docs/api/java.base/java/nio/channels/ServerSocketChannel.html) のAPI が変更されています。`open` メソッドの呼び出し時に [StandardProtocolFamily.UNIX](https://docs.oracle.com/en/java/javase/16/docs/api/java.base/java/net/StandardProtocolFamily.html#UNIX) を与えると、[SelectorProvider](https://docs.oracle.com/en/java/javase/16/docs/api/java.base/java/nio/channels/spi/SelectorProvider.html) が適切な実装を返してくれます。


## 目標

> The goal of this JEP is to support all of the features of Unix-domain sockets that are common across the major Unix platforms and Windows.

Windows でサポートされるようになったし、Unix と共通して使える部分は Java で実装してあげようよ、ということです。まさに、みんなが Java に期待していることをしてくれるわけですね。



# 使ってみた

動くサンプルを書いてみました。


## サーバー側

```UnixDomainServer.java
import java.io.IOException;
import java.net.StandardProtocolFamily;
import java.net.UnixDomainSocketAddress;
import java.nio.ByteBuffer;
import java.nio.channels.ServerSocketChannel;
import java.nio.channels.SocketChannel;
import java.nio.charset.StandardCharsets;
import java.nio.file.Files;
import java.nio.file.Path;

public class UnixDomainServer {
    public static final Path PATH = Path.of(System.getProperty("java.io.tmpdir"), ".yubaba");
    public static final UnixDomainSocketAddress ADDRESS = UnixDomainSocketAddress.of(PATH);

    public static void main(String[] args) throws IOException {
        try (ServerSocketChannel ssc = ServerSocketChannel.open(StandardProtocolFamily.UNIX)) {
            Files.deleteIfExists(PATH);
            ssc.bind(ADDRESS);
            try (SocketChannel socketChannel = ssc.accept()) {
                ByteBuffer buf = ByteBuffer.allocate(1024);
                socketChannel.read(buf);
                buf.flip();
                String introString = new String(buf.array(), StandardCharsets.UTF_8);
                System.out.println("She said, \"" + introString + "\"");
                String[] words = introString.split("\\s");
                String lastWord = words[words.length - 1];
                String name = lastWord.substring(0, lastWord.length() - 1);
                String declaration = "From now on, your name is %s!".formatted(yubaba(name));
                System.out.println(declaration);
                socketChannel.write(ByteBuffer.wrap(declaration.getBytes(StandardCharsets.UTF_8)));
            }
        }
    }

    private static String yubaba(String origName) {
        String[] graphemeClusters = origName.split("\\b{g}");
        return graphemeClusters[0];
    }
}

```

パスには一時ディレクトリを使っています。サーバーが湯婆婆なので、一時ディレクトリ内の `.yubaba` をアドレスとして使っています。PostgreSQL で設定ファイルに指定したディレクトリ内の `.s.PGSQL.5432` (5432 は TCP 用ポート番号) をアドレスとしていたので、真似ました。

Unix ドメイン ソケットは使い終わった後も 0 バイトのファイルのようなものが残ります。今回は終了時には削除せず、起動時に残っていたら削除するようにしています。

上記コードで、`buf.flip()` を忘れると、一見正しく動くのですが、実際にはたっぷりヌル文字が入ってしまいますのでご注意ください。私はやりました。

Unix ドメイン ソケットとは無関係ですが、今どきの湯婆婆は extended grapheme cluster を意識すると良いと思います。でも細かいことは考えていないので、表示できない文字とか `"` で始まる名前とかは名乗らないであげてください。


## クライアント側

```UnixDomainClient.java
public class UnixDomainClient {
    public static void main(String[] args) throws IOException {
        try (SocketChannel socketChannel = SocketChannel.open(StandardProtocolFamily.UNIX)) {
            socketChannel.connect(UnixDomainServer.ADDRESS);
            String introString = "My name is \uD83D\uDC69\u200D\uD83D\uDC69\u200D\uD83D\uDC66\u200D\uD83D\uDC66\uD83D\uDC7B.";
            System.out.println(introString);
            socketChannel.write(ByteBuffer.wrap(introString.getBytes(StandardCharsets.UTF_8)));
            ByteBuffer buf = ByteBuffer.allocate(1024);
            socketChannel.read(buf);
            buf.flip();
            System.out.println("She said, \"" + new String(buf.array(), StandardCharsets.UTF_8) + "\"");
        }
    }
}
```

今どきの子は emoji modifier を使った絵文字を名前に含めることにしました。宮崎アニメにこんな奇天烈な名前のキャラクターは出てこないと思いますが...。


## 動作確認

1. UnixDomainServer
2. UnixDomainClient

の順で実行します。

```plaintext:サーバー側
She said, "My name is 👩‍👩‍👦‍👦👻."
From now on, your name is 👩‍👩‍👦‍👦!
```

```plaintext:クライアント側
My name is 👩‍👩‍👦‍👦👻.
She said, "From now on, your name is 👩‍👩‍👦‍👦!"

```

このサンプルは1回メッセージをやりとりしたらそれで終わりです。

業務で使うなど、より本格的に利用する場合は [Inside Java の記事](https://inside.java/2021/02/03/jep380-unix-domain-sockets-channels/)に掲載のコードを参考にしてください。冒頭でご紹介したとおり、和訳もあります。


## 触れられなかったこと

[Inside Java の記事](https://inside.java/2021/02/03/jep380-unix-domain-sockets-channels/)では、Unix で [SO_PEERCRED](https://docs.oracle.com/en/java/javase/16/docs/api/jdk.net/jdk/net/ExtendedSocketOptions.html#SO_PEERCRED) により [UnixDomainPrincipal](https://docs.oracle.com/en/java/javase/16/docs/api/jdk.net/jdk/net/UnixDomainPrincipal.html) を得ることや、Docker での利用例、`inetd` や `launchd` で起動された時の [inherited channels](https://docs.oracle.com/en/java/javase/16/docs/api/java.base/java/nio/channels/spi/SelectorProvider.html#inheritedChannel()) の仕組みへの言及もあります。興味のある方は、ぜひそちらもご覧ください。


# まとめ

Java の標準ライブラリを使って同じマシン内で通信する手段が一つ増えました。コンテナとの通信などのプロセス間通信で活用される技術で、セキュリティ上の利点もあります。TCP ソケットのついでに Unix ドメイン ソケットも使えるようにしておくと喜ばれるかもしれません。


[^u]: W. Richard Stevens ほか (2014).『詳解UNIXプログラミング 第3版』. 翔泳社.