Share
Embed
実務で役立つTCPクライアントの作り方
Go Conference 2021 Spring (A9-S) のセッションで使用した資料です。
- セッションの詳細: https://gocon.jp/sessions/session-a9-s/
- 発表者: https://twitter.com/d_tutuz
資料に誤りがあればtwitterでご連絡ください。
Tsuji Daishiro
April 24, 2021
Transcript
36 slides
-
Who are you? 辻 大志郎(つじ だいしろう) @d_tutuz 渋谷区役所(~2014/9) Future(2014/10~) ✓
所属 Technology Innovation Group 競技プログラミング部 2 -
go-mcprotocol ✓ go-mcprotocol (@develop) ✓ PLCというハードウェア制御をソフトウェアで制御できる機器と通信 ✓ TCP上に、MELSECコミュニケーションプロトコル(通称:MCプロトコル)というプロトコルで通信 ✓ Go製のTCPクライアントライブラリ
4 https://emb.macnica.co.jp/articles/4277/ PLC -
netパッケージを使ったTCPの通信の基本 10 クライアント サーバー Listen() Accept() Dial() Write() Read() Read()
Write() net.Conn net.Conn -
netパッケージを使ったTCPの通信の基本 11 クライアント サーバー Listen() Accept() Dial() Write() Read() Read()
Write() Close() net.Conn net.Conn Close() -
参考 -netパッケージがシステムコールを内包- 12 クライアント サーバー Listen() Accept() Dial() Write() Read()
Read() Write() Close() net.Conn net.Conn Close() • socket() • bind() • listen() • accept() • read() • write() • close() • socket() • connect() -
GoでTCP Echoサーバ ✓ net.Listenを使って簡単にできる 14 func main() { ln, err
:= net.Listen("tcp", ":8080") if err != nil { log.Fatal(err) } for { conn, err := ln.Accept() if err != nil { log.Fatal(err) } go echoHandler(conn) } } func echoHandler(conn net.Conn) { defer conn.Close() io.Copy(conn, conn) } -
GoでTCPクライアント接続 ✓ net.Dialを使って簡単にできる 15 func main() { conn, err :=
net.Dial("tcp", "localhost:8080") if err != nil { log.Fatal(err) } _, err = conn.Write([]byte("hello 世界")) if err != nil { log.Fatal(err) } buf := make([]byte, 32) n, err := conn.Read(buf) if err != nil { log.Fatal(err) } fmt.Println(string(buf[:n])) } -
netパッケージを使ったタイムアウト(1/2) ✓ コネクションの確立時 ✓ net.DialTimeout(もしくはDialer構造体にTimeoutを設定)を用いる 20 func DialTimeout(network, address string,
timeout time.Duration) (Conn, error) { d := Dialer{Timeout: timeout} return d.Dial(network, address) } -
netパッケージを使ったタイムアウト(2/2) ✓ net.Connを使った読み書き時 ✓ SetDeadline(もしくはSetReadDeadlineやSetWriteDeadline)を用いる ✓ I/Oオペレーションの期限であって、タイムアウトではない ✓ 1つのI/Oごとにタイムアウトを設けるのであれば、期限を更新する必要あり 21
type Conn interface { Read(b []byte) (n int, err error) Write(b []byte) (n int, err error) Close() error LocalAddr() Addr RemoteAddr() Addr SetDeadline(t time.Time) error SetReadDeadline(t time.Time) error SetWriteDeadline(t time.Time) error } -
参考 -go-mcprotocolの場合- ✓ 構造体の中にタイムアウト時間を保持 22 // client3E is 3E frame
mcp client type client3E struct { // PLC address tcpAddr *net.TCPAddr // PLC station stn *station // Connect & Read Write timeout timeout time.Duration // TCP connection mu sync.Mutex conn net.Conn } -
参考 -go-mcprotocolの場合- ✓ タイムアウトが設定されていれば、最初に期限を設定 23 // Connection established if not
connect if err = c.connect(); err != nil { return nil, err } // Set write and read timeout if set timeout if c.timeout > 0 { deadline := time.Now().Add(c.timeout) if err = c.conn.SetDeadline(deadline); err != nil { _ = c.close() return nil, err } } // Send message if _, err = c.conn.Write(payload); err != nil { _ = c.close() return nil, err } -
コネクションプーリング ✓ コネクションを保持して再利用する ✓ 高速な通信が必要な場合 ✓ 高速化が不要であれば、都度コネクションを確立するのも一つの方法 ✓ 実装はよりシンプルに ✓
TCPのコネクション確立はコストが高い ✓ 3ウェイハンドシェイク ✓ サーバ側のコネクション確立+α 24 // client3E is 3E frame mcp client type client3E struct { // ... // TCP connection mu sync.Mutex conn net.Conn } -
並列処理される場合の考慮 26 ✓ net.Connはゴルーチンセーフ ✓ Write()やRead()している間に他のゴルーチンからClose()を呼び出すことが 可能 ✓ Write()している間にClose()されて困る場合はmutexで処理を保護 func
(c *client3E) Write(deviceName string, offset, numPoints int64, writeData []byte) ([]byte, error) { c.mu.Lock() defer c.mu.Unlock() requestStr := c.stn.BuildWriteRequest(deviceName, offset, numPoints, writeData) // … -
エラーハンドリング 28 ✓ io.EOF や syscall.EPIPE や net.ErrClosed(Go1.16~) などのハンドリング ✓
net.ErrClosedのおかげで“use of closed network connection”の文 字列が含まれるかどうかでチェックしなくて良くなった ✓ エラーが発生した場合に、プールしているコネクションをリリースする ✓ TCPライブラリとして提供する場合は、err != nil なときにプールしているコネク ションをリリースするのが親切 ✓ ライブラリを利用する側でエラー発生時にリリースすることもできるが -
go-mcprotocolの実装例 29 // Send message if _, err = c.conn.Write(payload);
err != nil { _ = c.close() return nil, err } ✓ エラーが発生した場合はライブラリ側でコネクションをリリース func (c *client3E) close() error { var err error if c.conn != nil { err = c.conn.Close() c.conn = nil } return err } -
リトライ 33 ✓ 瞬断はTCPの機構で救えそう ✓ (経験上)即時のリトライで救えることは少ない ✓ クライアントのリクエストが間違っている ✓ サーバが死んでいる
✓ ただし、コネクションプーリングしている場合は抱えているコネクションが悪く なっている場合がある ✓ アプリケーションの呼び元の責務でリトライするか判断すべき ✓ go-mcprotocol(TCPライブラリ)側では実装していない