JavaでUnix Domain Socketを使ってみる(Dockerのコンテナ一覧を取得する)

はじめに

Java 16でUnix Domain Socketのサポートが入っています(JEP 380)。前から興味はありつつもかなか触れてなかったので今回少し触ってみようかと思います。

お題として、以下の2つをやってみようと思います。

  • Echoサーバとそのクライアントを書く
  • JavaのプログラムからDockerを Unix Domain Socketを使って操作してみたいと思います。

Unix Domain Socket?

Unix Domain Socketは単一マシンで複数プロセスが、効率の良い通信を行なうためのソケットインターフェースです。 TCP/IPソケットと似たようなインターフェースですが、インターネットプロトコルの代わりにファイルシステムを利用しています。
以下のような特徴があるようです。

  • 同一マシン内で厳密にコミュニケーションを行い、リモートでの通信が必要ないためよりセキュアに通信できる
  • TCP/IPよりもセットアップが速い。スループットが高い

少し前にはなりますが、WindowsでもUnix Domain Socketサポートが入ったみたいです。

JavaUnix Domain Socketを使ってみる

環境

$ uname -srvmpio
Linux 5.4.0-89-generic #100-Ubuntu SMP Fri Sep 24 14:50:10 UTC 2021 x86_64 x86_64 x86_64 GNU/Linux

$ lsb_release -a
LSB Version:    core-11.1.0ubuntu2-noarch:security-11.1.0ubuntu2-noarch
Distributor ID: Ubuntu
Description:    Ubuntu 20.04.3 LTS
Release:    20.04
Codename:   focal

$ java --version
openjdk 17 2021-09-14
OpenJDK Runtime Environment (build 17+35-2724)
OpenJDK 64-Bit Server VM (build 17+35-2724, mixed mode, sharing)

Echoサーバーを書く

文字列を受け取って、その文字列に"Sent words are:"と言うプリフィクスを付けて返してプロセスを終了するシンプルなEchoサーバとそのクライアントを書いてみます。

ここを参考にコードを書いていきます。
基本的にはTCP/IPのソケットを開くときと似たような感じで利用できるみたいです。

サーバー側は以下のような感じ

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 UnixDomainSocketServer {

    public static void main(String[] args) {
        Path path = Path.of("/tmp", ".unixserver");
        UnixDomainSocketAddress socketAddress = UnixDomainSocketAddress.of(path);

        try (ServerSocketChannel server = ServerSocketChannel.open(StandardProtocolFamily.UNIX)) {
            Files.deleteIfExists(path);
            server.bind(socketAddress);
            ByteBuffer buf = ByteBuffer.allocate(1024);

            try (SocketChannel channel = server.accept()) {
                channel.read(buf);
                buf.flip();

                String input = StandardCharsets.UTF_8.decode(buf).toString();
                System.out.println("input = " + input);
                String response = "Sent words are: %s".formatted(input);
                channel.write(ByteBuffer.wrap(response.getBytes(StandardCharsets.UTF_8)));
            } catch (IOException e) {
                e.printStackTrace();
            }
        } catch (IOException e) {
            e.printStackTrace();
        }
    }
}

UnixDomainSocketAddressを使ってファイルシステムのアドレスをバインドすることできます。
また、Unix Domain Socketを利用するときはSocketを開く際にプロトコルファミリー(StandardProtocolFamily.UNIX))を指定してやる必要があるようです。

続いてクライアント側は以下のような感じです。

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

public class UnixDomainSocketClient {

    public static void main(String[] args) {
        Path path = Path.of("/tmp", ".unixserver");
        UnixDomainSocketAddress socketAddress = UnixDomainSocketAddress.of(path);

        try (final SocketChannel channel = SocketChannel.open(StandardProtocolFamily.UNIX)) {
            channel.connect(socketAddress);

            final String input = "Hello, UNIX Domain Socket";
            System.out.println("input = " + input);
            channel.write(ByteBuffer.wrap(input.getBytes(StandardCharsets.UTF_8)));

            ByteBuffer buf = ByteBuffer.allocate(1024);
            channel.read(buf);
            buf.flip();
            System.out.println("response = " + StandardCharsets.UTF_8.decode(buf));
        } catch (IOException e) {
            e.printStackTrace();
        }
    }
}

サーバと同様にUnixDomainSocketAddressを使ってサーバに繋ぐができます。

これらをサーバ→クライアントの順で実行すると、それぞれで以下のような出力を得られます。
サーバ側

input = Hello, UNIX Domain Socket

クライアント側

input = Hello, UNIX Domain Socket
response = Sent words are: Hello, UNIX Domain Socket

Dockerのコンテナ一覧を取得する

Docker エンジンはUnix Domain Socketを利用して、cURLなどで操作することができます。 各バージョンのAPIドキュメンテーションこちらから確認できます。
少しDockerのバージョンが低いですがcURLでDockerエンジンを操作するブログも過去に書いているのでよかったら読んでみてください。

今回は、コンテナの一覧を取得するJavaのコードを書いてみます。

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

public class DockerClient {
    public static void main(String[] args) {
        Path path = Path.of("/var/run", "docker.sock");
        UnixDomainSocketAddress socketAddress = UnixDomainSocketAddress.of(path);

        final String request = """
                GET /containers/json HTTP/1.1
                Host: localhost
                User-Agent: curl/7.68.0
                Accept: */*
                
                """;

        try (SocketChannel channel = SocketChannel.open(StandardProtocolFamily.UNIX)) {
            channel.connect(socketAddress);

            channel.write(ByteBuffer.wrap(request.getBytes(StandardCharsets.UTF_8)));

            ByteBuffer buf = ByteBuffer.allocate(1024);
            channel.read(buf);
            buf.flip();

            System.out.println("============= response ================");
            System.out.println(StandardCharsets.UTF_8.decode(buf));

        } catch (IOException e) {
            e.printStackTrace();
        }
    }
}

さほど複雑なことはしておらず、先程のクライアントのコードとほぼ同じです。ただHTTPのリクエスト形式で動いているコンテナの一覧を取得するリクエストを送っています。
cURLのリクエストをパクったので、User-Agentがcurlになってるのはご愛嬌ということで。。。。) このプログラムをコンテナが1つも動いていない状態で実行すると以下のような出力になります。

============= response ================
HTTP/1.1 200 OK
Api-Version: 1.41
Content-Type: application/json
Docker-Experimental: true
Ostype: linux
Server: Docker/20.10.9 (linux)
Date: Sat, 23 Oct 2021 06:03:09 GMT
Content-Length: 3

[]

今度は適当なコンテナを立ち上げて再度コードを実行してみます。

$  docker run --name tmp-nginx-container -d nginx
c1b1f0d91f368fcbe13113160dd06c1a755dc662e1d3ddfe82239e0775a3c6a1

$ docker ps
CONTAINER ID   IMAGE     COMMAND                  CREATED         STATUS         PORTS     NAMES
c1b1f0d91f36   nginx     "/docker-entrypoint.…"   4 seconds ago   Up 3 seconds   80/tcp    tmp-nginx-container

コードを実行すると今度は以下のような出力が得られ、実行されているコンテナの情報を取得が行えているのがわかります。

============= response ================
HTTP/1.1 200 OK
Api-Version: 1.41
Content-Type: application/json
Docker-Experimental: true
Ostype: linux
Server: Docker/20.10.9 (linux)
Date: Sat, 23 Oct 2021 06:07:44 GMT
Content-Length: 932

[{"Id":"c1b1f0d91f368fcbe13113160dd06c1a755dc662e1d3ddfe82239e0775a3c6a1","Names":["/tmp-nginx-container"],"Image":"nginx","ImageID":"sha256:08b152afcfae220e9709f00767054b824361c742ea03a9fe936271ba520a0a4b","Command":"/docker-entrypoint.sh nginx -g 'daemon off;'","Created":1634969175,"Ports":[{"PrivatePort":80,"Type":"tcp"}],"Labels":{"maintainer":"NGINX Docker Maintainers <docker-maint@nginx.com>"},"State":"running","Status":"Up About a minute","HostConfig":{"NetworkMode":"default"},"NetworkSettings":{"Networks":{"bridge":{"IPAMConfig":null,"Links":null,"Aliases":null,"NetworkID":"1ad0f3f401ba159560bd54403e670da4fd92e07b24f798cb2a97acc5b88e67d5","EndpointID":"f201ce886f9160d417fb4d9141253d591b54b9e2929884638321d057026f0436","Gateway":"172.17.0.1","IPAddress":"172.17.0.2","IPPrefixLen":16,"IPv6Gateway":"","Glob