ErlangでUNIXドメインソケットのクライアント接続を行なう簡単な方法

Erlang(OTP-17.3)では標準でUNIXドメインソケットをサポートされておらず、ちゃんと使おうとすると外部ライブラリが必要だったり、自前でポートドライバを書く必要があったりして、結構面倒。

ただ、特に性能等を気にせず簡単に使いたいだけなら、ncコマンド(netcat)がクライアント機能を持っているので、単にそれをラップすれば良い。

ncコマンドでのUNIXドメインソケット使用方法

-Uオプションを付けることで、UNIXドメインソケットが扱えるようになる。

サーバ:

# ※ 一つ以上のクライアントは接続不可
$ nc -U -l /tmp/hoge.socket

クライアント:

$ nc -U /tmp/hoge.socket

サーバとクライアントの通信は標準入出力経由で行なう。 

# サーバに"Hello World\n"とデータを送る
$ echo "Hello World\n" | nc -U /tmp/hoge.socket

Erlangから使う方法

erlang:open_port/2でncコマンドを呼び出すだけ。

-module(unix_socket).

-export([connect/1, send/2, close/1]).

%% @doc UNIXドメインソケット用のクライアント接続関数
-spec connect(string()) -> {ok, port()} | {error, Reason::term()}.
connect(UnixDomainSocketPath) ->
    %% open_port/2を使って、ncコマンドを呼び出す
    Command = "/bin/nc",
    Port = erlang:open_port({spawn_executable, Command},
                            [{args, ["-U", UnixDomainSocketPath]},
                             stderr_to_stdout, binary, exit_status]),
    receive
        %% 50ms以内にコマンドが停止したら、connect失敗扱いにする
        {Port, {exit_status, Status}} ->
            receive
                {Port, {data, ExitReason}} ->
                    {error, {abort, Command, [{status, Status}, {reason, ExitReason}]}}
            after 0 ->
                    {error, {abort, Command, [{status, Status}]}}
            end
    after 50 ->
            %% 接続成功
            {ok, Port}
    end.

-spec send(port(), iodata()) -> ok | {error, Reason::term()}.
send(Port, Data) ->
    try
        _ = erlang:port_command(Port, Data),
        ok
    catch
        error:Reason ->
            {error, Reason}
    end.

-spec close(port()) -> ok.
close(Port) ->
    erlang:port_close(Port).

使用例:

%% 事前に別のシェルで`nc -U -l /tmp/hoge.socket`が実行されているものとする

%% 接続失敗: 存在しないパスを指定
> unix_socket:connect("/tmp/fuga.socket").
{error,{abort,"/bin/nc",
              [{status,1},
               {reason,<<"nc: unix connect failed: No such file or directory\n">>}]}}

%% 接続成功
> {ok, S} = unix_socket:connect("/tmp/hoge.socket").
{ok,#Port<0.23824>}

%% データ送信
> unix_socket:send(S, <<"Hello\n">>). % サーバ側の端末に"Hello\n"と表示される
ok

%% データ受信:
%%   サーバ側の端末で"World\n"という文字列が入力されたものとする
> flush().
Shell got {#Port<0.23824>,{data,<<"World\n">>}}
ok

%% 切断
> unix_socket:close(S).
ok

デバッグ用途等であれば使えなくはない、程度のメモ書き。