JavaでUnix Domain Socketを使ってみる(Dockerのコンテナ一覧を取得する)
はじめに
Java 16でUnix Domain Socketのサポートが入っています(JEP 380)。前から興味はありつつもかなか触れてなかったので今回少し触ってみようかと思います。
お題として、以下の2つをやってみようと思います。
Unix Domain Socket?
Unix Domain Socketは単一マシンで複数プロセスが、効率の良い通信を行なうためのソケットインターフェースです。
TCP/IPソケットと似たようなインターフェースですが、インターネットプロトコルの代わりにファイルシステムを利用しています。
以下のような特徴があるようです。
少し前にはなりますが、WindowsでもUnix Domain Socketサポートが入ったみたいです。
JavaでUnix 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