LoginSignup
11
11

More than 5 years have passed since last update.

ErlangでUNIX domain socketを使う

Posted at

ErlangでUNIX domain socketに接続しようと思ったら、Erlangの標準では接続できなかった。なので、接続するための方法を調べた時のことを書く。
また、時間の都合もあり、ClientとしてUNIX domain socketに接続する方法を中心に調べた。

UNIX domain socketが使えるLibrary

まず、ネットを検索してみると、UNIX domain socketを使うためのライブラリはGitHub上にいくらかあった。

GitHub repository

  • procket
    • procket is an Erlang library for socket creation and manipulation.
    • 様々なSocketを使うためのライブラリ
  • unixdom_drv
    • This is a very incomplete reimplementation of the UNIX domain socket driver that I wrote and added to the www.erlang.org "User Contributions" collection.
    • UNIX domain socketを使うためのライブラリ
  • basho/enm
    • enm is an Erlang port driver that wraps the nanomsg C library, allowing Erlang systems to communicate with other nanomsg endpoints. enm supports idioms and approaches common to standard Erlang networking facilities such as gen_tcp and gen_udp.
    • C言語で書かれたnanomsgをwrapしたライブラリ。
  • afunix
    • afunix is an api to unix domain sockets. The afunix is a "plugin" to the inet/gen_tcp. The api is binary compatible with the gen_tcp interface. The afunix is not available on the windows platform, hence the unix part of afunix.
    • UNIX domain socketを使うためのライブラリ
      • 1番シンプル?

その他

ncコマンドでUNIX domain socketに接続することもできる。それを利用して、以下のように外部コマンド呼び出しでUNIX domain socketと接続する方法もある。

標準Packagesにexamplesとして入っている "uds_dist"

Erlang OTPにuds_distというライブラリがある。これは、 ${OTPROOT}/lib/kernel/examples の中にある(GitHub)。
ただし、 examples とあるように、標準ではloadされていないし、コード自体も怪しい。Cのコードが約1,000行、Erlangのコードが約600行なので、見てみることにした。

使用

まず、src/uds_server.erl内の erlang:info(machine) だけ使えないので、 erlang:system_info(machine) に修正する必要がある。

Makefileのサンプルもあるが、rebar.configを作って、rebarの実行ファイルを持って来てコンパイルできるようにした。
主に下記の2設定を書いておけば良い。(他は通常通り)

rebar.config
{port_specs,
  [
   {"priv/lib/uds_drv.so", ["c_src/*.c"]}
  ]}.

{port_env,
  [
   {".*", "CFLAGS", "$CFLAGS -O3 -g -fPIC -pedantic -Wall -Werror -Ic_src/"}
  ]}.

UNIX domain socketを(クライアントとして)使いたい場合は、以下のようになる。

$ ./rebar shell
1> uds_server:start_link().
{ok,<0.43.0>}
% 開かれているUnix domain socketのpathは"/tmp/erlang/unix_domain_socket_path"とする
2> {ok, Port} = uds:connect("unix_domain_socket_path").
{ok,#Port<0.920>}
3> uds:send(Port, "send message").
{ok,#Port<0.920>}
4> uds:close(Port).
ok
5> 

※今回、uds_dist.erl周りは時間の関係上終えていない

制限

"/tmp/erlang" 以下しか、待ち受け/接続できない

define部分 - GitHub

uds_drv.c
#define SOCKET_PATH "/tmp/erlang"

この部分の設定より、 /tmp/erlang/ 以下にしかUNIX domain socketを作ることができない。この制限をなくすには SOCKET_PATH を空文字にすればよい。(ただし、Erlangの呼び出し側でUNIX domain socketのパスの最初の / を除く必要がある( /tmp/socket に接続したい場合は、 "tmp/socket" )。嫌だったらコードを修正。

4byteのSize Headerが付く

define部分 - GitHub

uds_drv.c
#define HEADER_LENGTH 4

send関数

uds_drv.c
static void do_send(UdsData *ud, char *buff, int bufflen) 
{
    char header[4];
    int written;
    SysIOVec iov[2];
    ErlIOVec eio;
    ErlDrvBinary *binv[] = {NULL,NULL};

    put_packet_length(header, bufflen);
    DEBUGF(("Write packet header %u,%u,%u,%u.", (Word) header[0],
       (Word) header[1], (Word) header[2],(Word) header[3]));
    iov[0].iov_base = (char *) header;
    iov[0].iov_len = 4;
    iov[1].iov_base = buff;
    iov[1].iov_len = bufflen;
    eio.iov = iov;
    eio.binv = binv;
    eio.vsize = 2;
    eio.size = bufflen + 4;
    written = 0;
    if (driver_sizeq(ud->port) == 0) {
    if ((written = writev(ud->fd, iov, 2)) == eio.size) {

recv関数

uds_drv.c
static int buffered_read_package(UdsData *ud, char **result)
{
    int res;
    int data_size;

    if (ud->buffer_pos < ud->header_pos + HEADER_LENGTH) {
    /* The header is not read yet */
    DEBUGF(("Header not read yet"));
    if ((res = read_at_least(ud, ud->header_pos + HEADER_LENGTH - 
                 ud->buffer_pos)) < 0) {
        DEBUGF(("Header read failed"));
        return res;
    }
    } 
    DEBUGF(("Header is read"));
    /* We have at least the header read */
    data_size = get_packet_length((char *) ud->buffer + ud->header_pos);
    DEBUGF(("Input packet size = %d", data_size));
    if (ud->buffer_pos < ud->header_pos + HEADER_LENGTH + data_size) {
    /* We need to read more */
    DEBUGF(("Need to read more (bufferpos %d, want %d)", ud->buffer_pos,
        ud->header_pos + HEADER_LENGTH + data_size));
    if ((res = read_at_least(ud, 
                 ud->header_pos + HEADER_LENGTH + 
                 data_size - ud->buffer_pos)) < 0) {
        DEBUGF(("Data read failed"));
        return res;
    }
    }
    DEBUGF(("Data is completely read"));
    *result = (char *) ud->buffer + ud->header_pos + HEADER_LENGTH;
    ud->header_pos += HEADER_LENGTH + data_size;
    return data_size;
}

上に適当にコードを抜粋してきたが、このように、通信内容に4byteのSize Headerを付ける/付いている前提で実装されている。しかも、Sizeを変える/Size Headerを付けない場合、定数の値を修正するだけでは動かない(主にsend周り)。Erlangのコードで実装されたアプリケーション同士がこのライブラリを使ってUNIX domain socketで通信する場合は問題がないが(そのような状況があるのかは別として)、仕様を変えたい場合はある程度コードを修正する必要がある。

Port drivers

最後に、コードを見るレベルでPort Driverがどういったことをしているか簡単に書いておく。

ErlDrvEntry - GitHub

ErlDrvEntry
ErlDrvEntry uds_driver_entry = {
    NULL,          /* init, N/A */
    uds_start,             /* start, called when port is opened */
    uds_stop,              /* stop, called when port is closed */
    uds_command,           /* output, called when erlang has sent */
    uds_input,             /* ready_input, called when input descriptor ready */
    uds_output,            /* ready_output, called when output descriptor ready */
    "uds_drv",             /* char *driver_name, the argument to open_port */
    uds_finish,            /* finish, called when unloaded */
    NULL,                  /* void * that is not used (BC) */
    uds_control,           /* control, port_control callback */
    NULL,                  /* timeout, called on timeouts */
    NULL,                  /* outputv, vector output interface */
    NULL,                  /* ready_async */
    NULL,                  /* flush */
    NULL,                  /* call */
    NULL,                  /* event */
    ERL_DRV_EXTENDED_MARKER,
    ERL_DRV_EXTENDED_MAJOR_VERSION,
    ERL_DRV_EXTENDED_MINOR_VERSION,
    0,  /* ERL_DRV_FLAGs */
    NULL,
    NULL,                  /* process_exit */
    uds_stop_select
};

まず、上記のようにErlDrvEntryが定義されていて、ここにどんなイベントが起きたら、どの関数が呼ばれるかが書かれている。

  • uds_start / uds_stop
    • portがopen/closeした時に呼ばれる関数
  • uds_command
    • Erlangで port_command が呼ばれた時に呼び出される関数
  • uds_input / uds_output
    • driver_select しているdiscriptorに入出力があった時に呼ばれる関数
  • etc...

それぞれインターフェースが決まっているが、それは公式ドキュメント参照とする。

また、上記関数の引数として取られている ErlDrvData は、ここで定義されているポインタである。

erl_driver.h
typedef struct _erl_drv_data* ErlDrvData; /* Data to be used by the driver itself. */

ここには適当に持ち回すデータを入れるだけみたいだ。uds_distでは次のようになっている。

uds_drv.h
typedef struct uds_data {
    int fd;                   /* File descriptor */
    ErlDrvPort port;          /* The port identifier */
    int lockfd;               /* The file descriptor for a lock file in case of listen sockets */
    Byte creation;            /* The creation serial derived from the lockfile */
    PortType type;            /* Type of port */
    char *name;               /* Short name of socket for unlink */
    Word sent;                /* Messages sent */
    Word received;            /* Messages received */
    struct uds_data *partner; /* The partner in an accept/listen pair */
    struct uds_data *next;    /* Next structure in list */

    /* The input buffer and it's data */
    int buffer_size;          /* The allocated size of the input buffer */
    int buffer_pos;           /* Current position in input buffer */
    int header_pos;           /* Where the current header is in the input buffer */
    Byte *buffer;            /* The actual input buffer */
} UdsData;

この構造体が各関数の先頭でキャストされて使われている。

後は、uds_server.erl で erl_dll:load_driver/2 して、 erlang:port_command/2 等の実行により処理されている。

最後に

このような感じに、自分で一から(examplesを見ているということはおいておいて)ErlangでUNIX domain socket周りを実装するとなると、結構苦労することが多い。また、「プログラミングErlang p.178」にも書かれているのだが、「リンクインドライバで何か重大なエラーが起こると、Erlangシステムがクラッシュし、システムのすべてのプロセスに影響が出る」(プログラミングErlangの文章を引用)ので、「ご利用は計画的に」。(Segmentation Faultが出るとErlang VMが落ちる…gkbr)

11
11
0

Register as a new user and use Qiita more conveniently

  1. You get articles that match your needs
  2. You can efficiently read back useful information
  3. You can use dark theme
What you can do with signing up
11
11