UNIX domain socket

なんかしらそれなりにリソース喰う物体はサーバ化しといてみんなでそのリソース使う…っていうのはよくやることかと思います。うちの場合 cmigemo と rdic と w3mcooksrv がそんな感じ。 w3mcooksrv は cookie 共有したいからだけど。

で w3mcooksrv 作る時に使ってみてから、 UNIX domain socket は割とハンディで便利だなぁと気付きました。

  • TCP に比べて port の番号を決めなくて良いのが良い。
  • named pipe に比べてちゃんとトランザクションがあるのが良い。

以下例のごとくにわか知識で覚え書き。

サーバ

C だとお決まりの socket bind listen accept で作る。以下同時接続数 1 の echo サーバ。

 #include <stdio.h>
 #include <unistd.h>
 #include <sys/types.h>
 #include <sys/socket.h>
 #include <sys/un.h>
 
 int main() {
     int sock;
     struct sockaddr_un addr;
 
     if ((sock = socket(PF_UNIX, SOCK_STREAM, 0)) < 0) {
         perror("socket");
         return 1;
     }
 
     memset(&addr, 0, sizeof(addr));
     addr.sun_family = PF_UNIX;
     strcpy(addr.sun_path, "/tmp/hoge");
     if (bind(sock, (struct sockaddr*)&addr, sizeof(addr)) < 0) {
         perror("bind");
         return 1;
     }
 
     if (listen(sock, 1) < 0) {
         perror("listen");
         return 1;
     }
 
     while (1) {
         struct sockaddr_un dummy;
         int fd;
         int len = sizeof(dummy);
 
         if ((fd = accept(sock, (struct sockaddr*)&dummy, &len)) < 0) {
             perror("accept");
             return 1;
         }
 
         while (1) {
             int ret;
             char c;
             ret = read(fd, &c, 1);
             if (ret < 0) {
                 perror("read");
                 return 1;
             }
             else if (ret == 0) {
                 break;
             }
             write(fd, &c, 1);
         }
         close(fd);
     }
 }

はしょって書いたのに長いな…これでも同時接続できないし。 Ruby は UNIXServer なるクラスを使うとほとんど TCPServer と同じでラク。

 require 'socket'
 
 USOCK = '/tmp/hoge'
 
 if File.exist? USOCK
   File.unlink USOCK
 end
 
 UNIXServer.open(USOCK) do |serv|
   while true
     s = serv.accept
     while true
       c = s.getc
       if c
         s.putc c
       else
         break
       end
     end
   end
 end

こんくらいだとちゃんとスレッド作って同時接続対応とかしてやってもいい気がしてきます。

でもまぁもっとラクな方法があって、 Ubuntu だと ucspi-unix ってパッケージに入ってる unixserver コマンドを使うと

 unixserver /tmp/hoge cat

で終わり。ちゃんと終了時にソケット削除してくれるし良い。接続数設定とかもできるし。要は tcpserver とかみたいに標準入出力とやりとりするコマンドがなんでもサーバになってくれるわけ。なんか tcpserver が手元に入ってないと気付いたので tcpsvd とやらで代用すると、

 tcpsvd 0 9999 cat

とかとおなじ。

クライアント

telnet -u が便利…だと思ったら MacOSXFreeBSD には telnet -u あるんだけど、手元の Linux には無いみたいで困る。 unixclient では作れないかなぁ…と思ったので Ruby でサックリと。

 require 'socket'
 
 sock = UNIXSocket.open(ARGV[0])
 
 inputs = [STDIN, sock]
 
 while true
   i, o, e = IO.select(inputs)
   i.each do |input|
     if input == STDIN
       c = STDIN.getc
       if c
         sock.putc(c)
         sock.flush
       else
         sock.close_write
         inputs = [sock]
       end
     elsif input == sock
       c = sock.getc
       if c
         STDOUT.putc(c)
       else
         exit
       end
     end
   end
 end

ucspi-unix には unixcat ってのがついていて、それで良さげに思えるんだけど、中身見ると

 exec unixclient "$1" sh -c 'exec cat <&6'

とかいう shell script (unixclient は unixserver のクライアントバージョンで、指定したプログラムと fd 6 と 7 を使ってやりとりする) で、これは unix domain socket の吐いた内容の出力はできるけど入力はできないと思う。入力をやるなら、

 cat >&7
 cat <&6

とかを実行させればいい気がするんだけど、なんにせよこれらだと接続終了時に終了しない。これはたぶんサーバ側の cat が終わってないからで、一個目の cat が終了した後に終わらない理由は書き込み側の終了を伝えてないからだと思う。 7 を閉じちゃえばいいようにも思えるんだけど、これ socket だから 6 も 7 も同じファイルディスクリプタで書き込み側とかの区別がなくて、閉じちゃえば接続が切れてしまって今度は受信ができない。 shell から shutdown(fd, SHUT_WR); 的なことができるといいんだけど。

このへんの事情は TCP socket も同じで、よくわからんけど、読み書きを一つのファイルディスクリプタでやるっていう設計のせいなのかなぁ。 TODO: Plan9 とかどうやってるんだろう。

なにかあれば下記メールアドレスへ。
shinichiro.hamaji _at_ gmail.com
shinichiro.h