いろいろ備忘録日記

主に .NET とか Go とか Flutter とか Python絡みのメモを公開しています。

Goメモ-103 (Go で Unix domain socket (AF_UNIX) のメモ)

概要

知らなかったのですが、Go1.12 から Windows でも Unix domain socket (AF_UNIX) が使えるようになったのですね。

qiita.com

github上のissueを検索してみると見つかりました。これですね。

github.com

Windows10 から、AF_UNIX がサポートされたみたいですね。知らなかった。。。

mattn.kaoriya.net

Windowsの世界にいると、Unix domain socket (AF_UNIX)には縁がないと思います。

Unix domain socket はこんな感じのものです。

ja.wikipedia.org

単一のマシンの中で利用できる効率の良いプロセス間通信の方法と覚えておけばいいと思います。

ちょこっと遊んでみました。

すみません。上でWindowsの話をしているのですが、以下のサンプルは Linux 上で作って動作させました。

Goでの処理の仕方

Goで Unix domain socket で通信行う場合に特別なやり方は特になくて、いつもの net.Listen()net.Dial()

  • プロトコルに unix を指定
  • アドレスの部分にソケットを表すファイルパスを指定

という形になります。後は、いつもの通信処理と同じです。こんな感じ。

サーバ

listener, err := net.Listen("unix", "/tmp/echo.sock")
if err != nil {
    log.Fatal(err)
}

クライアント

conn, err := net.Dial("unix", "/tmp/echo.sock")
if err != nil {
    log.Fatal(err)
} 

サンプルソースについて

今回のサンプルですが、ついでなので github にアップしてあります。よろしければ、ご参照ください。

github.com

基本的な使い方のサンプル

シンプルな形のサンプル。通信のプロトコルも無しってことで、1ショットでクライアントから送って応答もらって終わり。

プロトコルないので、クライアントから送った後にサーバからのレスポンスもらうために、無理やりWrite側のストリームを

閉じるって荒業しています。サンプル以外ではしない方がいいですね。

サーバ側

ソケットを開いてAccept待ちをし、Connectしてきたらデータを受信して大文字にして返すだけのサーバです。

一回処理したら、コネクションを切断しています。(これも本来は良くないです)

package main

import (
   "bytes"
   "fmt"
   "io"
   "log"
   "net"
   "os"
   "os/signal"
   "strings"
)

const (
   protocol = "unix"
   sockAddr = "/tmp/echo.sock"
)

// https://eli.thegreenplace.net/2019/unix-domain-sockets-in-go/
func main() {
    cleanup := func() {
        if _, err := os.Stat(sockAddr); err == nil {
            if err := os.RemoveAll(sockAddr); err != nil {
                log.Fatal(err)
            }
        }
    }

    cleanup()

    listener, err := net.Listen(protocol, sockAddr)
    if err != nil {
        log.Fatal(err)
    }
    defer listener.Close()

    quit := make(chan os.Signal)
    signal.Notify(quit, os.Interrupt)

    go func() {
        <-quit
        fmt.Println("ctrl-c pressed..")
        close(quit)
        cleanup()
        os.Exit(0)
    }()

    fmt.Println("server launched...")
    for {
        conn, err := listener.Accept()
        if err != nil {
            log.Fatal(err)
        }

        fmt.Println(">>> accepted")
        go echo(conn)
    }
}

func echo(conn net.Conn) {
    defer conn.Close()
    log.Printf("Connected: %s\n", conn.RemoteAddr().Network())

    buf := &bytes.Buffer{}
    _, err := io.Copy(buf, conn)
    if err != nil {
        log.Println(err)
        return
    }

    s := strings.ToUpper(buf.String())

    buf.Reset()
    buf.WriteString(s)

    _, err = io.Copy(conn, buf)
    if err != nil {
        log.Println(err)
        return
    }

    fmt.Println("<<< ", s)
}

クライアント側

接続したら、hello world ってデータを送って、応答を受け取り表示しています。1回毎にコネクションを切断して毎回接続しにいってます。

package main

import (
   "fmt"
   "io/ioutil"
   "log"
   "net"
   "time"
)

const (
   protocol = "unix"
   sockAddr = "/tmp/echo.sock"
)

func main() {
    for i := 0; i < 5; i++ {
        time.Sleep(1 * time.Second)

        conn, err := net.Dial(protocol, sockAddr)
        if err != nil {
            log.Fatal(err)
        }

        _, err = conn.Write([]byte("hello world"))
        if err != nil {
            log.Fatal(err)
        }

        // サーバ側にクライアントからの書き込みが終わったことを
        // 無理やり伝えるためにWrite側のソケットを強制クローズ
        // (サンプル以外ではしてはいけない)
        err = conn.(*net.UnixConn).CloseWrite()
        if err != nil {
            log.Fatal(err)
        }

        b, err := ioutil.ReadAll(conn)
        if err != nil {
            log.Fatal(err)
        }

        fmt.Println(string(b))
    }
}

サーバ側では何も考えずに io.Copy(buf, conn) って形でやっているので、クライアント側から無理やり書き込みストリームを切っているのが

err = conn.(*net.UnixConn).CloseWrite()

の部分ですね。ここが無いと、クライアント側の処理は先に進みません。

通信仕様がサーバと1ショットの要求/応答で終わる場合はこれでもいいかもしれませんが、あまりしない方がいいです。

動作確認

動かしてみます。ターミナルを2つ起動します。

片方はサーバ、片方がクライアントとなります。

サーバ起動。

まずはサーバから起動。

$ go run .
server launched...

と表示されて、Accept待ちになります。Unix domain socket はファイルを使ってソケット通信します。

なので、net.Listen() で指定したパスのファイルが出来ているはずです。

$ ls -l /tmp/echo.sock
srwxr-xr-x 1 devlights devlights 0  823 18:30 /tmp/echo.sock

このファイルは特殊なファイルです。file コマンドで見てみると

$ file /tmp/echo.sock
/tmp/echo.sock: socket

ちゃんとソケットって表示されますね。

もう少し、細かい情報をみてみましょう。

$ lsof /tmp/echo.sock
COMMAND  PID      USER   FD   TYPE             DEVICE SIZE/OFF  NODE NAME
server  6702 devlights    3u  unix 0x00000000da2811b6      0t0 35285 /tmp/echo.sock type=STREAM

PIDは 6702 ってなっていますね。ファイルディスクリプタ(FD)は、標準入出力以外は 3 以降が割り当てられます。

また、3の後ろに u (読み書き両用)となっています。なので、双方向の通信が可能です。

PIDが分かったので、実行本体は誰なのか見てみます。

$ lsof -p 6702 | grep txt
server  6702 devlights  txt       REG               0,51  2162688 223237 /tmp/go-build255060087/b001/exe/server

TYPEがtxt (Program text) のものを見ると、ちゃんと server って名前のプログラムになってます。

クライアント起動

では、クライアント側を起動します。

$ go run .
HELLO WORLD
HELLO WORLD
HELLO WORLD
HELLO WORLD
HELLO WORLD

内部では 小文字で hello world って送ったものがサーバからの応答で大文字になって返ってきていますね。

サーバ側のコンソールは以下のように出力されます。

>>> accepted
2020/08/23 18:30:17 Connected: unix
<<<  HELLO WORLD
>>> accepted
2020/08/23 18:30:18 Connected: unix
<<<  HELLO WORLD
>>> accepted
2020/08/23 18:30:19 Connected: unix
<<<  HELLO WORLD
>>> accepted
2020/08/23 18:30:20 Connected: unix
<<<  HELLO WORLD
>>> accepted
2020/08/23 18:30:21 Connected: unix
<<<  HELLO WORLD
通信内容を覗いてみる

通信プログラムは何のデータを送受信しているか分からないとデバッグなどがちゃんと出来ません。

流しているデータも超シンプルなので、どんな形で流れているか確認しましょう。

Unix domain socket の通信データを覗く場合は、通常よく利用するtcpdump などでは簡単にみれません。

socat というコマンドを使って覗きましょう。 socat が入っていない場合は

$ sudo apt install -y socat

とかでインストールしておいてください。

インストールが終わったら、もう一つターミナルを開いて、以下のようにします。ちょっと長いですが、、w

$ mv /tmp/echo.sock /tmp/echo.sock.original
$ socat -t100 -x -v UNIX-LISTEN:/tmp/echo.sock,mode=777,reuseaddr,fork UNIX-CONNECT:/tmp/echo.sock.original

これで、このターミナルに通信データが出力されます。

これでもう一度、クライアント側を起動すると以下のような出力が得られます。

> 2020/08/23 18:28:19.584580  length=11 from=0 to=10
 68 65 6c 6c 6f 20 77 6f 72 6c 64                 hello world
--
< 2020/08/23 18:28:19.590375  length=11 from=0 to=10
 48 45 4c 4c 4f 20 57 4f 52 4c 44                 HELLO WORLD
--
> 2020/08/23 18:28:20.600967  length=11 from=0 to=10
 68 65 6c 6c 6f 20 77 6f 72 6c 64                 hello world
--
< 2020/08/23 18:28:20.605547  length=11 from=0 to=10
 48 45 4c 4c 4f 20 57 4f 52 4c 44                 HELLO WORLD
--
> 2020/08/23 18:28:21.612451  length=11 from=0 to=10
 68 65 6c 6c 6f 20 77 6f 72 6c 64                 hello world
--
< 2020/08/23 18:28:21.615758  length=11 from=0 to=10
 48 45 4c 4c 4f 20 57 4f 52 4c 44                 HELLO WORLD
--
> 2020/08/23 18:28:22.623844  length=11 from=0 to=10
 68 65 6c 6c 6f 20 77 6f 72 6c 64                 hello world
--
< 2020/08/23 18:28:22.626016  length=11 from=0 to=10
 48 45 4c 4c 4f 20 57 4f 52 4c 44                 HELLO WORLD
--
> 2020/08/23 18:28:23.633889  length=11 from=0 to=10
 68 65 6c 6c 6f 20 77 6f 72 6c 64                 hello world
--
< 2020/08/23 18:28:23.637283  length=11 from=0 to=10
 48 45 4c 4c 4f 20 57 4f 52 4c 44                 HELLO WORLD
--

当たり前ですが、そのままのデータが流れていますね。毎回接続して切断してるので、 from は 毎回 0 となっています。

メッセージ用の構造体を作って通信仕様に従って通信 (1)

上のサンプルは基本的な動作のためのものなので、通信仕様がありませんでした。

さすがに適当すぎるので、もう少しマシなサンプルを。以下のようなメッセージ仕様とします。

  • 最初の4バイトにデータ長を持つ
  • データ長フィールドの後ろに実際のデータ(文字列)が続く
  • エンディアンはビッグエンディアン

構造体定義

上の通信メッセージを表現する構造体を定義します。

(余談ですが、私は通信メッセージとか言わずに電文っていう方がしっくり来る世代です....)

package message

import (
    "bytes"
    "encoding/binary"
    "fmt"
    "net"
)

type (
    Echo struct {
        Length int
        Data   []byte
    }
)

func (e *Echo) String() string {
    return fmt.Sprintf("Length[%02d] Data[%s]", e.Length, e.Data)
}

func (e *Echo) Write(c net.Conn) error {
    data := make([]byte, 0, 4+e.Length)

    buf := make([]byte, 4)
    binary.BigEndian.PutUint32(buf, uint32(e.Length))
    data = append(data, buf...)

    w := bytes.Buffer{}
    err := binary.Write(&w, binary.BigEndian, e.Data)
    if err != nil {
        return err
    }

    data = append(data, w.Bytes()...)

    _, err = c.Write(data)
    if err != nil {
        return err
    }

    return nil
}

func (e *Echo) Read(c net.Conn) error {
    buf := make([]byte, 4)

    _, err := c.Read(buf)
    if err != nil {
        return err
    }

    byteCount := binary.BigEndian.Uint32(buf)
    e.Length = int(byteCount)
    e.Data = make([]byte, e.Length)

    _, err = c.Read(e.Data)
    if err != nil {
        return err
    }

    return nil
}

サーバもクライアントも同じエンコード、デコード処理をすることになるのでメソッドとして定義しておきました。

サーバ側

処理の流れは最初のサンプルと同じです。データの送受信部分で上の構造体を使っています。

package main

import (
   "fmt"
   "log"
   "net"
   "os"
   "os/signal"
   "strings"

   "github.com/devlights/go-unix-domain-socket-example/pkg/message"
)

const (
   protocol = "unix"
   sockAddr = "/tmp/echo.sock"
)

func main() {
    if _, err := os.Stat(sockAddr); err == nil {
        if err := os.RemoveAll(sockAddr); err != nil {
            log.Fatal(err)
        }
    }

    listener, err := net.Listen(protocol, sockAddr)
    if err != nil {
        log.Fatal(err)
    }

    quit := make(chan os.Signal)
    signal.Notify(quit, os.Interrupt)

    go func() {
        <-quit
        fmt.Println("ctrl-c pressed!")
        close(quit)
        os.Exit(0)
    }()

    fmt.Println("> Server launched")
    for {
        fmt.Println("> wait...")

        conn, err := listener.Accept()
        if err != nil {
            log.Fatal(err)
        }

        fmt.Println(">>> accepted: ", conn.RemoteAddr().Network())
        go echo(conn)
    }
}

func echo(conn net.Conn) {
    defer conn.Close()

    m := &message.Echo{}
    err := m.Read(conn)
    if err != nil {
        log.Println(err)
        return
    }

    s := strings.ToUpper(string(m.Data))
    m.Length = len(s)
    m.Data = []byte(s)

    err = m.Write(conn)
    if err != nil {
        log.Println(err)
        return
    }
}

クライアント側

クライアントも流れは最初のサンプルと同じです。データの送受信部分で上の構造体を使っています。

package main

import (
   "fmt"
   "log"
   "net"
   "time"

   "github.com/devlights/go-unix-domain-socket-example/pkg/message"
)

const (
   protocol = "unix"
   sockAddr = "/tmp/echo.sock"
)

func main() {
    values := []string{
        "hello world",
        "golang",
        "goroutine",
        "this program runs on crostini",
    }

    for _, v := range values {
        time.Sleep(1 * time.Second)

        conn, err := net.Dial(protocol, sockAddr)
        if err != nil {
            log.Fatal(err)
        }

        func() {
            defer conn.Close()

            m := &message.Echo{
                Length: len(v),
                Data:   []byte(v),
            }

            err = m.Write(conn)
            if err != nil {
                log.Fatal(err)
            }

            err = m.Read(conn)
            if err != nil {
                log.Fatal(err)
            }

            fmt.Printf("%v\n", m)
        }()
    }
}

今回は、ちゃんと仕様に則って通信しているので、無理やり書き込みストリームを閉じるとかはしなくても大丈夫。

動作確認

動かしてみます。ターミナルを2つ起動します。

片方はサーバ、片方がクライアントとなります。

サーバ起動。

まずはサーバから起動。

$ go run .
> Server launched
> wait...

と表示されて、Accept待ちになります。

クライアント起動

では、クライアント側を起動します。

$ go run .
Length[11] Data[HELLO WORLD]
Length[06] Data[GOLANG]
Length[09] Data[GOROUTINE]
Length[29] Data[THIS PROGRAM RUNS ON CROSTINI]

ちゃんとデータ長を設定して送受信出来ていますね。

通信内容を覗いてみる
$ mv /tmp/echo.sock /tmp/echo.sock.original
$ socat -t100 -x -v UNIX-LISTEN:/tmp/echo.sock,mode=777,reuseaddr,fork UNIX-CONNECT:/tmp/echo.sock.original

これでもう一度、クライアント側を起動すると以下のような出力が得られます。

> 2020/08/23 18:34:46.086687  length=15 from=0 to=14
 00 00 00 0b 68 65 6c 6c 6f 20 77 6f 72 6c 64     ....hello world
--
< 2020/08/23 18:34:46.093024  length=15 from=0 to=14
 00 00 00 0b 48 45 4c 4c 4f 20 57 4f 52 4c 44     ....HELLO WORLD
--
> 2020/08/23 18:34:47.107126  length=10 from=0 to=9
 00 00 00 06 67 6f 6c 61 6e 67                    ....golang
--
< 2020/08/23 18:34:47.112755  length=10 from=0 to=9
 00 00 00 06 47 4f 4c 41 4e 47                    ....GOLANG
--
> 2020/08/23 18:34:48.121788  length=13 from=0 to=12
 00 00 00 09 67 6f 72 6f 75 74 69 6e 65           ....goroutine
--
< 2020/08/23 18:34:48.126190  length=13 from=0 to=12
 00 00 00 09 47 4f 52 4f 55 54 49 4e 45           ....GOROUTINE
--
> 2020/08/23 18:34:49.134952  length=33 from=0 to=32
 00 00 00 1d 74 68 69 73 20 70 72 6f 67 72 61 6d  ....this program
 20 72 75 6e 73 20 6f 6e 20 63 72 6f 73 74 69 6e   runs on crostin
 69                                               i
--
< 2020/08/23 18:34:49.143030  length=33 from=0 to=32
 00 00 00 1d 54 48 49 53 20 50 52 4f 47 52 41 4d  ....THIS PROGRAM
 20 52 55 4e 53 20 4f 4e 20 43 52 4f 53 54 49 4e   RUNS ON CROSTIN
 49                                               I
--

最初の4バイトにデータ長が設定されて、その後にデータ部が続いています。

メッセージ用の構造体を作って通信仕様に従って通信 (2)

ここまでのサンプルは、毎回1回毎に接続と切断を繰り返していました。

実際は、一回接続を張ったら何度も送受信する通信もあります。

てことで、先ほどのサンプルを接続したまま、複数回送受信するようにします。

構造体定義

利用するメッセージは同じです。

サーバ側

処理の流れは一つ前のサンプルと同じです。

今回はクライアントが切断してくるまで処理を続けます。現実では相手がいつまでも切ってこないとか、いろいろ考慮しないといけないのですが割愛。

package main

import (
   "fmt"
   "io"
   "log"
   "net"
   "os"
   "os/signal"
   "strings"

   "github.com/devlights/go-unix-domain-socket-example/pkg/message"
)

const (
   protocol = "unix"
   sockAddr = "/tmp/echo.sock"
)

func main() {
    cleanup := func() {
        if _, err := os.Stat(sockAddr); err == nil {
            if err := os.RemoveAll(sockAddr); err != nil {
                log.Fatal(err)
            }
        }
    }

    cleanup()

    listener, err := net.Listen(protocol, sockAddr)
    if err != nil {
        log.Fatal(err)
    }

    quit := make(chan os.Signal)
    signal.Notify(quit, os.Interrupt)

    go func() {
        <-quit
        fmt.Println("ctrl-c pressed!")
        close(quit)
        cleanup()
        os.Exit(0)
    }()

    fmt.Println("> Server launched")
    for {
        conn, err := listener.Accept()
        if err != nil {
            log.Fatal(err)
        }

        fmt.Println(">>> accepted: ", conn.RemoteAddr().Network())
        go echo(conn)
    }
}

func echo(conn net.Conn) {
    defer conn.Close()

    for {
        m := &message.Echo{}
        err := m.Read(conn)
        if err != nil {
            if err == io.EOF {
                fmt.Println("=== closed by client")
                break
            }

            log.Println(err)
            break
        }

        fmt.Println("[READ ] ", m)

        s := strings.ToUpper(string(m.Data))
        m.Length = len(s)
        m.Data = []byte(s)

        err = m.Write(conn)
        if err != nil {
            log.Println(err)
            break
        }

        fmt.Println("[WRITE] ", m)
    }
}

クライアント側

クライアントも流れは一つ前のサンプルと同じです。

今回は毎回切断せずに、一つのコネクション上で複数回データを流します。

package main

import (
   "fmt"
   "log"
   "net"
   "time"

   "github.com/devlights/go-unix-domain-socket-example/pkg/message"
)

const (
   protocol = "unix"
   sockAddr = "/tmp/echo.sock"
)

func main() {
    values := []string{
        "hello world",
        "golang",
        "goroutine",
        "this program runs on crostini",
    }

    conn, err := net.Dial(protocol, sockAddr)
    if err != nil {
        log.Fatal(err)
    }
    defer conn.Close()

    for _, v := range values {
        time.Sleep(1 * time.Second)

        m := &message.Echo{
            Length: len(v),
            Data:   []byte(v),
        }

        err = m.Write(conn)
        if err != nil {
            log.Fatal(err)
        }

        fmt.Println("[WRITE] ", m)

        err = m.Read(conn)
        if err != nil {
            log.Fatal(err)
        }

        fmt.Println("[READ ] ", m)
    }
}

動作確認

動かしてみます。ターミナルを2つ起動します。

片方はサーバ、片方がクライアントとなります。

サーバ起動。

まずはサーバから起動。

$ go run .
> Server launched

と表示されて、Accept待ちになります。

クライアント起動

では、クライアント側を起動します。

$ go run .
[WRITE]  Length[11] Data[hello world]
[READ ]  Length[11] Data[HELLO WORLD]
[WRITE]  Length[06] Data[golang]
[READ ]  Length[06] Data[GOLANG]
[WRITE]  Length[09] Data[goroutine]
[READ ]  Length[09] Data[GOROUTINE]
[WRITE]  Length[29] Data[this program runs on crostini]
[READ ]  Length[29] Data[THIS PROGRAM RUNS ON CROSTINI]

サーバ側のコンソールは以下のように出力されます。

>>> accepted:  unix
[READ ]  Length[11] Data[hello world]
[WRITE]  Length[11] Data[HELLO WORLD]
[READ ]  Length[06] Data[golang]
[WRITE]  Length[06] Data[GOLANG]
[READ ]  Length[09] Data[goroutine]
[WRITE]  Length[09] Data[GOROUTINE]
[READ ]  Length[29] Data[this program runs on crostini]
[WRITE]  Length[29] Data[THIS PROGRAM RUNS ON CROSTINI]
=== closed by client
通信内容を覗いてみる
$ mv /tmp/echo.sock /tmp/echo.sock.original
$ socat -t100 -x -v UNIX-LISTEN:/tmp/echo.sock,mode=777,reuseaddr,fork UNIX-CONNECT:/tmp/echo.sock.original

これでもう一度、クライアント側を起動すると以下のような出力が得られます。

> 2020/08/23 18:35:59.850381  length=15 from=0 to=14
 00 00 00 0b 68 65 6c 6c 6f 20 77 6f 72 6c 64     ....hello world
--
< 2020/08/23 18:35:59.859045  length=15 from=0 to=14
 00 00 00 0b 48 45 4c 4c 4f 20 57 4f 52 4c 44     ....HELLO WORLD
--
> 2020/08/23 18:36:00.868576  length=10 from=15 to=24
 00 00 00 06 67 6f 6c 61 6e 67                    ....golang
--
< 2020/08/23 18:36:00.869875  length=10 from=15 to=24
 00 00 00 06 47 4f 4c 41 4e 47                    ....GOLANG
--
> 2020/08/23 18:36:01.874743  length=13 from=25 to=37
 00 00 00 09 67 6f 72 6f 75 74 69 6e 65           ....goroutine
--
< 2020/08/23 18:36:01.877597  length=13 from=25 to=37
 00 00 00 09 47 4f 52 4f 55 54 49 4e 45           ....GOROUTINE
--
> 2020/08/23 18:36:02.881073  length=33 from=38 to=70
 00 00 00 1d 74 68 69 73 20 70 72 6f 67 72 61 6d  ....this program
 20 72 75 6e 73 20 6f 6e 20 63 72 6f 73 74 69 6e   runs on crostin
 69                                               i
--
< 2020/08/23 18:36:02.886945  length=33 from=38 to=70
 00 00 00 1d 54 48 49 53 20 50 52 4f 47 52 41 4d  ....THIS PROGRAM
 20 52 55 4e 53 20 4f 4e 20 43 52 4f 53 54 49 4e   RUNS ON CROSTIN
 49                                               I
--

今回は、切断せずに送受信してるので、 from の値が上がっていってますね。

encoding/gob パッケージを使って通信

ここまでのサンプルは、エンコードとデコードの処理を自前で行っていました。

このサンプルは、双方向がGoプログラム同士なので encoding/gob パッケージを

つかってエンコードとデコードの部分は gob に任せてしまいましょう。

golang.org

構造体定義

利用するメッセージは同じです。

サーバ側

処理の流れは一つ前のサンプルと同じです。

違いは、送受信の際に Echo.Read, Echo.Writeメソッドを使わずに gob に任せてしまっている点です。

package main

import (
   "encoding/gob"
   "fmt"
   "io"
   "log"
   "net"
   "os"
   "os/signal"
   "strings"

   "github.com/devlights/go-unix-domain-socket-example/pkg/message"
)

const (
   protocol = "unix"
   sockAddr = "/tmp/echo.sock"
)

func main() {
    cleanup := func() {
        if _, err := os.Stat(sockAddr); err == nil {
            if err := os.RemoveAll(sockAddr); err != nil {
                log.Fatal(err)
            }
        }
    }

    cleanup()

    listener, err := net.Listen(protocol, sockAddr)
    if err != nil {
        log.Fatal(err)
    }

    quit := make(chan os.Signal)
    signal.Notify(quit, os.Interrupt)

    go func() {
        <-quit
        fmt.Println("ctrl-c pressed!")
        close(quit)
        cleanup()
        os.Exit(0)
    }()

    fmt.Println("> Server launched")
    for {
        conn, err := listener.Accept()
        if err != nil {
            log.Fatal(err)
        }

        fmt.Println(">>> accepted: ", conn.RemoteAddr().Network())
        go echo(conn)
    }
}

func echo(conn net.Conn) {
    defer conn.Close()

    decoder := gob.NewDecoder(conn)
    encoder := gob.NewEncoder(conn)
    for {
        m := &message.Echo{}
        err := decoder.Decode(m)
        if err != nil {
            if err == io.EOF {
                fmt.Println("=== closed by client")
                break
            }

            log.Println(err)
            break
        }

        fmt.Println("[READ ] ", m)

        s := strings.ToUpper(string(m.Data))
        m.Length = len(s)
        m.Data = []byte(s)

        err = encoder.Encode(m)
        if err != nil {
            log.Println(err)
            break
        }

        fmt.Println("[WRITE] ", m)
    }
}

クライアント側

クライアントも流れは一つ前のサンプルと同じです。

こちらもサーバ側と同じくエンコードとデコードは gob に任せています。

package main

import (
   "encoding/gob"
   "fmt"
   "log"
   "net"
   "time"

   "github.com/devlights/go-unix-domain-socket-example/pkg/message"
)

const (
   protocol = "unix"
   sockAddr = "/tmp/echo.sock"
)

func main() {
    values := []string{
        "hello world",
        "golang",
        "goroutine",
        "this program runs on crostini",
    }

    conn, err := net.Dial(protocol, sockAddr)
    if err != nil {
        log.Fatal(err)
    }
    defer conn.Close()

    decoder := gob.NewDecoder(conn)
    encoder := gob.NewEncoder(conn)

    for _, v := range values {
        time.Sleep(1 * time.Second)

        m := &message.Echo{
            Length: len(v),
            Data:   []byte(v),
        }

        err = encoder.Encode(m)
        if err != nil {
            log.Fatal(err)
        }

        fmt.Println("[WRITE] ", m)

        err = decoder.Decode(m)
        if err != nil {
            log.Fatal(err)
        }

        fmt.Println("[READ ] ", m)
    }
}   

動作確認

動かしてみます。ターミナルを2つ起動します。

片方はサーバ、片方がクライアントとなります。

サーバ起動。

まずはサーバから起動。

$ go run .
> Server launched

と表示されて、Accept待ちになります。

クライアント起動

では、クライアント側を起動します。

$ go run .
[WRITE]  Length[11] Data[hello world]
[READ ]  Length[11] Data[HELLO WORLD]
[WRITE]  Length[06] Data[golang]
[READ ]  Length[06] Data[GOLANG]
[WRITE]  Length[09] Data[goroutine]
[READ ]  Length[09] Data[GOROUTINE]
[WRITE]  Length[29] Data[this program runs on crostini]
[READ ]  Length[29] Data[THIS PROGRAM RUNS ON CROSTINI]

gob がちゃんと必要なデータをエンコード、デコードしてくれていますね。

サーバ側のコンソールは以下のように出力されます。

>>> accepted:  unix
[READ ]  Length[11] Data[hello world]
[WRITE]  Length[11] Data[HELLO WORLD]
[READ ]  Length[06] Data[golang]
[WRITE]  Length[06] Data[GOLANG]
[READ ]  Length[09] Data[goroutine]
[WRITE]  Length[09] Data[GOROUTINE]
[READ ]  Length[29] Data[this program runs on crostini]
[WRITE]  Length[29] Data[THIS PROGRAM RUNS ON CROSTINI]
=== closed by client

見た目は一つ前のサンプルと同じですが、流れている通信データはどのようになっているでしょうか。

通信内容を覗いてみる
$ mv /tmp/echo.sock /tmp/echo.sock.original
$ socat -t100 -x -v UNIX-LISTEN:/tmp/echo.sock,mode=777,reuseaddr,fork UNIX-CONNECT:/tmp/echo.sock.original

これでもう一度、クライアント側を起動すると以下のような出力が得られます。

> 2020/08/23 18:37:52.694433  length=58 from=0 to=57
 26 ff 81 03 01 01 04 45 63 68 6f 01 ff 82 00 01  &......Echo.....
 02 01 06 4c 65 6e 67 74 68 01 04 00 01 04 44 61  ...Length.....Da
 74 61 01 0a                                      ta..
 00 00 00 12 ff 82 01 16 01 0b 68 65 6c 6c 6f 20  ..........hello 
 77 6f 72 6c 64 00                                world.
--
< 2020/08/23 18:37:52.706073  length=58 from=0 to=57
 26 ff 81 03 01 01 04 45 63 68 6f 01 ff 82 00 01  &......Echo.....
 02 01 06 4c 65 6e 67 74 68 01 04 00 01 04 44 61  ...Length.....Da
 74 61 01 0a                                      ta..
 00 00 00 12 ff 82 01 16 01 0b 48 45 4c 4c 4f 20  ..........HELLO 
 57 4f 52 4c 44 00                                WORLD.
--
> 2020/08/23 18:37:53.712358  length=14 from=58 to=71
 0d ff 82 01 0c 01 06 67 6f 6c 61 6e 67 00        .......golang.
--
< 2020/08/23 18:37:53.715074  length=14 from=58 to=71
 0d ff 82 01 0c 01 06 47 4f 4c 41 4e 47 00        .......GOLANG.
--
> 2020/08/23 18:37:54.718886  length=17 from=72 to=88
 10 ff 82 01 12 01 09 67 6f 72 6f 75 74 69 6e 65  .......goroutine
 00                                               .
--
< 2020/08/23 18:37:54.724753  length=17 from=72 to=88
 10 ff 82 01 12 01 09 47 4f 52 4f 55 54 49 4e 45  .......GOROUTINE
 00                                               .
--
> 2020/08/23 18:37:55.731878  length=37 from=89 to=125
 24 ff 82 01 3a 01 1d 74 68 69 73 20 70 72 6f 67  $...:..this prog
 72 61 6d 20 72 75 6e 73 20 6f 6e 20 63 72 6f 73  ram runs on cros
 74 69 6e 69 00                                   tini.
--
< 2020/08/23 18:37:55.738594  length=37 from=89 to=125
 24 ff 82 01 3a 01 1d 54 48 49 53 20 50 52 4f 47  $...:..THIS PROG
 52 41 4d 20 52 55 4e 53 20 4f 4e 20 43 52 4f 53  RAM RUNS ON CROS
 54 49 4e 49 00                                   TINI.
--

流れているデータが少し変わりましたね。

サーバとクライアントがお互い最初の通信部分でデータ型の情報を伝えています。

ここは gob がやっている部分です。対向先で正しくエンコード、デコードするためには型の情報が必要なのでこうなります。

また、各通信単位でも少しデータサイズが増えていますね。これも gob が必要な情報を付与しているためです。

通信データサイズが少し増えてしまいますが、デコードとエンコードの事を考慮しなくていいのはとても楽です。

Go同士のプログラムが通信する際は gob はアリだなって思いました。

参考情報

eli.thegreenplace.net

johnrefior.com

superuser.com

おすすめ書籍

自分が読んだGo関連の本で、いい本って感じたものです。

Go言語による並行処理

Go言語による並行処理

スターティングGo言語 (CodeZine BOOKS)

スターティングGo言語 (CodeZine BOOKS)

  • 作者:松尾 愛賀
  • 発売日: 2016/04/15
  • メディア: 単行本(ソフトカバー)

プログラミング言語Go (ADDISON-WESLEY PROFESSIONAL COMPUTING SERIES)

プログラミング言語Go (ADDISON-WESLEY PROFESSIONAL COMPUTING SERIES)


過去の記事については、以下のページからご参照下さい。

  • いろいろ備忘録日記まとめ

devlights.github.io

サンプルコードは、以下の場所で公開しています。

  • いろいろ備忘録日記サンプルソース置き場

github.com

github.com

github.com