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と接続する方法もある。
- [ErlangでUNIXドメインソケットのクライアント接続を行なう簡単な方法 - sileのブログ]
(http://sile.hatenablog.jp/entry/2014/11/27/042638)
標準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設定を書いておけば良い。(他は通常通り)
{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 SOCKET_PATH "/tmp/erlang"
この部分の設定より、 /tmp/erlang/
以下にしかUNIX domain socketを作ることができない。この制限をなくすには SOCKET_PATH
を空文字にすればよい。(ただし、Erlangの呼び出し側でUNIX domain socketのパスの最初の /
を除く必要がある( /tmp/socket
に接続したい場合は、 "tmp/socket"
)。嫌だったらコードを修正。
4byteのSize Headerが付く
#define HEADER_LENGTH 4
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) {
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 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
が呼ばれた時に呼び出される関数
- Erlangで
-
uds_input
/uds_output
-
driver_select
しているdiscriptorに入出力があった時に呼ばれる関数
-
- etc...
それぞれインターフェースが決まっているが、それは公式ドキュメント参照とする。
また、上記関数の引数として取られている ErlDrvData
は、ここで定義されているポインタである。
typedef struct _erl_drv_data* ErlDrvData; /* Data to be used by the driver itself. */
ここには適当に持ち回すデータを入れるだけみたいだ。uds_distでは次のようになっている。
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)