- 概要
- Goでの処理の仕方
- サンプルソースについて
- 基本的な使い方のサンプル
- メッセージ用の構造体を作って通信仕様に従って通信 (1)
- メッセージ用の構造体を作って通信仕様に従って通信 (2)
- encoding/gob パッケージを使って通信
- 参考情報
- おすすめ書籍
概要
知らなかったのですが、Go1.12 から Windows でも Unix domain socket (AF_UNIX) が使えるようになったのですね。
github上のissueを検索してみると見つかりました。これですね。
Windows10 から、AF_UNIX がサポートされたみたいですね。知らなかった。。。
Windowsの世界にいると、Unix domain socket (AF_UNIX)には縁がないと思います。
Unix domain socket はこんな感じのものです。
単一のマシンの中で利用できる効率の良いプロセス間通信の方法と覚えておけばいいと思います。
ちょこっと遊んでみました。
すみません。上で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 にアップしてあります。よろしければ、ご参照ください。
基本的な使い方のサンプル
シンプルな形のサンプル。通信のプロトコルも無しってことで、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 8月 23 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 に任せてしまいましょう。
構造体定義
利用するメッセージは同じです。
サーバ側
処理の流れは一つ前のサンプルと同じです。
違いは、送受信の際に 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 はアリだなって思いました。
参考情報
おすすめ書籍
自分が読んだGo関連の本で、いい本って感じたものです。
- 作者:Katherine Cox-Buday
- 発売日: 2018/10/26
- メディア: 単行本(ソフトカバー)
- 作者:松尾 愛賀
- 発売日: 2016/04/15
- メディア: 単行本(ソフトカバー)
プログラミング言語Go (ADDISON-WESLEY PROFESSIONAL COMPUTING SERIES)
- 作者:Alan A.A. Donovan,Brian W. Kernighan
- 発売日: 2016/06/20
- メディア: 単行本(ソフトカバー)
過去の記事については、以下のページからご参照下さい。
- いろいろ備忘録日記まとめ
サンプルコードは、以下の場所で公開しています。
- いろいろ備忘録日記サンプルソース置き場