このページの本文へ

前へ 1 2 次へ

Goならわかるシステムプログラミング 第9回

Unixドメインソケット

2017年01月11日 09時00分更新

文● 渋川よしき、編集● 鹿野桂一郎

  • この記事をはてなブックマークに追加
  • 本文印刷

今回はソケット通信の最終回ということで、Unixドメインソケットについて紹介します。 UnixドメインソケットはPOSIX系OSで提供されている機能です。 コンピュータ内部でしか使えない代わりに、高速に通信が行えます。 TCP型(ストリーム型)と、UDP型(データグラム型)の両方の使い方ができます。

WindowsではこのUnixドメインソケットをそのまま使うことはできません。 似た概念の機能として、「名前付きパイプ」というものが存在します。 以降の説明では、まずUnixドメインソケットについて説明したあとで、 Windowsの名前付きパイプについても説明します。 (Windowsメインの方は、前半のUnixドメインソケット固有の説明については軽く流して読んでください。)

Unixドメインソケットの基本

TCPとUDPによるソケット通信は、外部のネットワークに繋がるインタフェースに接続します。 これに対し、Unixドメインソケットでは外部インタフェースへの接続は行いません。 その代わり、カーネル内部で完結する高速なネットワークインタフェースを作成します。 Unixドメインソケットを使うことで、ウェブサーバとNGINXなどのリバースプロキシとの間、あるいはウェブサーバとデータベースとの間の接続を高速にできる場合があります。

Unixドメインソケットを開くには、ファイルシステムのパスを指定します。 サーバプロセスを起動すると、ファイルシステム上の指定した位置にファイルができます。 そのサーバが作成したファイルに、クライアントプロセスから繋ぎに行きます。 クライアントは、通常のソケット通信のようにIPアドレスとポート番号を使って相手を探すのではなく、 ファイルパスを使って通信相手を探します。 ファイルのパーミッションを使ってアクセス制限を加えることも可能です。

一見すると、単にサーバが作成したファイルにクライアントが書き込み、その内容を他のプロセスが見にいっているだけに思えるかもしれません。 しかし、そうではありません。 Unixドメインソケットで作成されるのは、ソケットファイルという特殊なファイルであり、通常のファイルのような実態はありません。 あくまでもプロセス間の高速な通信としてファイルというインタフェースを利用するだけです。

● ファイルパス以外でUnixドメインソケットを作成するには

Unixドメインソケットには、本来はファイルパスを指定する以外の作成方法もあります。ただし、Go言語からは使えません。

syscall.Socketpair()
無名のUnixドメインソケットのペアを作成します。C言語のプログラムであれば、この後にプログラムをフォーク(プロセスのコピーを作成)することで、親プロセス・子プロセス間の高速な通信手段として使うことができます。 Go言語ではUnix環境であっても単純なフォークをサポートしていません。そのため、この関数を有効に使う手段は、ファイルディスクリプタを別プロセスに送信する以外には今のところ存在しません。 Go言語で単純なフォークがサポートされてない理由については将来の連載記事で紹介する予定です。
抽象名前空間
Linuxだけの機能になりますが、C言語ではNULLバイトが先頭に入っていると、ファイルパスと関係ない名前でソケットを作成します。Goの場合は、"\u0000" で始まる名前を使うことで使用できます。

Unixドメインソケットの使い方

Unixドメインソケットの使い方は簡単です。 基本的には、いままでに説明したTCP/UDPのソケットと同様に使えます。

ストリーム型のUnixドメインソケット

まずは、TCPと同等のストリーム型のUnixドメインソケットのGo言語で使う方法です。

クライアント側のコードは次のような構成です。

conn, err := net.Dial("unix", "socketfile")
if err != nil {
  panic(err)
}
// connを使った読み書き

サーバ側の最低限のコードはこのようになります。

listener, err := net.Listen("unix", "socketfile")
if err != nil {
  panic(err)
}
defer listener.Close()
conn, err := listener.Accept()
if err != nil {
  // エラー処理
}
// connを使った読み書き

どちらも、本連載の第6回「GoでたたくTCPソケット(前編)」で説明した TCPのときのコードとほとんど同じであることがわかると思います。

サーバでは、ストリーム型のソケットをTCPと同様にnet.Listen()を使って開きます。 TCPとの違いは、net.Listen()の第一引数が"tcp"ではなく"unix"である点と、第二引数がアドレスとポートではなく「ファイルのパス」という点です。 返ってくるインタフェースはTCPの場合と同じnet.Listener であり、使い勝手もまったく変わりません。

クライアント側では、やはりTCPのときと同じく、net.Dial()を使います。 net.Dial()の最初の引数に指定するソケットの型は、サーバ側と同じくunixです。 第二引数には、これもサーバ側と同じくファイルのパスを指定します。

TCPとの違いとして注意すべき点として、サーバ側でnet.Listener.Close()を呼ばないとソケットファイルが残り続けてしまうことが挙げられます。 Ctrl+Cで止める場合には、何らかの方法でシグナルをトラップする必要があるでしょう。 シグナルについては、次回以降の記事で紹介しますので、今回はとりあえず「ファイルがすでにあってソケットが開けない」というエラーが出たら手でファイルを削除してください。 きちんとClose()したいときは、「Graceful Shutdown」あたりのキーワードでウェブ検索すれば実現方法が見つかるでしょう。 ソケットファイルをオープンする前に既存のファイルを削除してみる、という手も定石としてよく使われます。

// 削除(存在しなかったらしなかったで問題ないのでエラーチェックは不要)
os.Remove(path)

実は、ソケットを正しくクローズした場合にソケットファイルが削除されるのはGo言語特有の処理です。C言語だと、Unixドメインソケットを使った場合、close()を呼んでもソケットファイルが残ります。

この処理を行うGo言語のソースコードは下記にあります。

上記のGo言語のソースコードでは、"@"で名前が始まるケースを特別扱いし、抽象名前空間で作られたソケットを除外しています。

HTTPサーバとクライアントをUnixドメインソケットで作る

本連載の第6回「GoでたたくTCPソケット(前編)」で紹介したHTTPサーバとクライアントを、Unixドメインソケットで書き換えてみましょう。

まずはHTTPサーバです。TCP版のimport文に"path/filepath""os"を書き足し、net.Listen()の引数を変更していきます。 TEMP領域の"unixdomainsocket-sample"というファイルパスにソケットファイルが作られるようにします。

package main
import (
  "bufio"
  "fmt"
  "io/ioutil"
  "net"
  "net/http"
  "net/http/httputil"
  "os"
  "path/filepath"
  "strings"
)
func main() {
  path := filepath.Join(os.TempDir(), "unixdomainsocket-sample")
  os.Remove(path)
  listener, err := net.Listen("unix", path)
  if err != nil {
    panic(err)
  }
  defer listener.Close()
  fmt.Println("Server is running at " + path)
  for {
    conn, err := listener.Accept()
    if err != nil {
      panic(err)
    }
    go func() {
      fmt.Printf("Accept %v\n", conn.RemoteAddr())
      // リクエストを読み込む
      request, err := http.ReadRequest(bufio.NewReader(conn))
      if err != nil {
        panic(err)
      }
      dump, err := httputil.DumpRequest(request, true)
      if err != nil {
        panic(err)
      }
      fmt.Println(string(dump))
      // レスポンスを書き込む
      response := http.Response{
        StatusCode: 200,
        ProtoMajor: 1,
        ProtoMinor: 0,
        Body:       ioutil.NopCloser(strings.NewReader("Hello World\n")),
      }
      response.Write(conn)
      conn.Close()
    }()
  }
}

このサーバを起動したら、使っているシステムのTEMP領域のフォルダを見てみましょう。 作成されたソケットのファイルがあることがわかります。 先頭の文字"s"はソケットファイルであることを表しています。

$ ls -la unixdomainsocket-sample
srwxr-xr-x    1 shibu  1522739515     0 Jan  5 00:47 unixdomainsocket-sample

作成されたソケットについての情報は、netstatコマンドを使って見ることができます。

$ netstat -u
Active LOCAL (Unix) domain sockets
Address          Type   Recv-Q Send-Q            Inode Conn Refs Nextref Addr
24c652c93b976ddf stream      0      0 24c652c93b081ae7    0    0       0 /var/.../unixdomainsocket-sample

基本的にはこれだけです。

クライアント側の実装も見てみましょう。

package main
import (
  "bufio"
  "fmt"
  "net"
  "net/http/httputil"
  "os"
  "path/filepath"
)
func main() {
  conn, err := net.Dial("unix", 
    filepath.Join(os.TempDir(), "unixdomainsocket-sample"))
  if err != nil {
    panic(err)
  }
  request, err := http.NewRequest(
                    "get", "http://localhost:8888", nil)
  if err != nil {
    panic(err)
  }
  request.Write(conn)
  response, err := http.ReadResponse(
                     bufio.NewReader(conn), request)
  if err != nil {
    panic(err)
  }
  dump, err := httputil.DumpResponse(response, true)
  if err != nil {
    panic(err)
  }
  fmt.Println(string(dump))
}

"unix""unixpacket"

net.Listen()net.Dial()に指定したソケットの型としては、"unix"のほかに"unixpacket"というものもあります。 "unix""unixpacket"の違いですが、システムコールの文脈では"unix"SOCK_STREAMで、 "unixpacket"SOCK_SEQPACKETという定数です。 SOCK_SEQPACKETは、TCPのようにコネクション指向だけど通信はUDPのようなデータグラム単位で扱うというソケットの型です。 SOCK_SEQPACKETは新しめのOSでないと実装されていません。 macOSでも実装されておらず、指定すると1エラーになります。

実装されているOSであっても、実はGo言語から簡単に使えません。 前回の記事では紹介しませんでしたが、UDPなどのデータグラム型のソケットに対して使用できるsyscall.Recvmsg()というシステムコールがあります。 これは、UDPで紹介したPacketConn.ReadFrom()の高機能版で、制御コードも受信できます。 このシステムコールでデータ境界を取得できるというのが、SOCK_SEQPACKETのウリとされています。 この制御コードのパース処理はsyscall.ParseSocketControlMessage()などの関数を使って行うことができます。 今回は割愛します。

データグラム型のUnixドメインソケット

次に、UDP相当の使い方ができるデータグラム型のUnixドメインソケットの実装について紹介します。

まずはサーバです。 前回のUDPのサンプルと同じくnet.ListenPacket()を使いますが、プロトコルとして"udp"ではなく"unixgram"を指定し、アドレスとしてはファイルパスを渡します。 それ以外の部分は、基本的には前回のコードから修正していません。

package main
import (
  "fmt"
  "net"
  "os"
  "path/filepath"
)
func main() {
  path := filepath.Join(os.TempDir(), "unixdomainsocket-server")
  // エラーチェックは削除(存在しなかったらしなかったで問題ないので不要)
  os.Remove(path)
  fmt.Println("Server is running at " + path)
  conn, err := net.ListenPacket("unixgram", path)
  if err != nil {
    panic(err)
  }
  defer conn.Close()
  buffer := make([]byte, 1500)
  for {
    length, remoteAddress, err := conn.ReadFrom(buffer)
    if err != nil {
      panic(err)
    }
    fmt.Printf("Received from %v: %v\n", 
               remoteAddress, string(buffer[:length]))
    _, err = conn.WriteTo([]byte("Hello from Server"), 
                          remoteAddress)
    if err != nil {
      panic(err)
    }
  }
}

それではクライアント側も修正してみましょう。 これまでの傾向を見れば、net.Dial()の引数を変更するだけでよさそうですよね? だとしたら、次のようなコードになるはずです。

conn, err := net.Dial("unixgram", filepath.Join(os.TempDir(), "unixdomainsocket-server"))

さっそく試してみましょう。

Server is running at /var/folders/.../T/unixdomainsocket-server
Received from <nil>: Hello from Client
panic: write unixgram /var/folders/.../unixdomainsocket-server: invalid argument

なんということでしょう。エラーになってしまいました。 エラーの原因は、サーバ側のconn.ReadFrom()呼び出しで取得できるアドレスがnilになってしまい、返事を送り返せないことです。 net.Dial()で開いたソケットは一方的な送信用で、アドレス(ソケットファイルのパス)と結び付けられていないので、返信を受けられないのです。

解決方法は、クライアント側もサーバと同じ初期化を行い、net.PacketConnインタフェースのWriteTo()メソッドと、ReadFrom()メソッドを使って送受信することです。 送信を、自分の受信用のソケットファイルを持っているソケットから実行すれば、サーバのReadFrom()で返信可能なアドレスが得られます。 これによりUDPとほぼ同じ要領でサーバを利用できるようになります。

package main
import (
  "log"
  "net"
  "os"
  "path/filepath"
)
func main() {
  clientPath := filepath.Join(os.TempDir(), "unixdomainsocket-client")
  // エラーチェックは不要なので削除(存在しなかったらしなかったで問題ない)
  os.Remove(clientPath)
  conn, err := net.ListenPacket("unixgram", clientPath)
  if err != nil {
    panic(err)
  }
  // 送信先のアドレス
  unixServerAddr, err := net.ResolveUnixAddr(
    "unixgram", filepath.Join(os.TempDir(), "unixdomainsocket-server"))
  var serverAddr net.Addr = unixServerAddr
  if err != nil {
    panic(err)
  }
  defer conn.Close()
  log.Println("Sending to server")
  _, err = conn.WriteTo([]byte("Hello from Client"), serverAddr)
  if err != nil {
    panic(err)
  }
  log.Println("Receiving from server")
  buffer := make([]byte, 1500)
  length, _, err := conn.ReadFrom(buffer)
  if err != nil {
    panic(err)
  }
  log.Printf("Received: %s\n", string(buffer[:length]))
}

脚注

  1. http://stackoverflow.com/questions/13287333/sock-seqpacket-availability

前へ 1 2 次へ

カテゴリートップへ

この連載の記事
ピックアップ