Upgrade to Pro — share decks privately, control downloads, hide ads and more …

実務で役立つTCPクライアントの作り方

 実務で役立つ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
Tweet

More Decks by Tsuji Daishiro

Other Decks in Technology

Transcript

  1. 参考 -netパッケージがシステムコールを内包- 12 クライアント サーバー Listen() Accept() Dial() Write() Read()

    Read() Write() Close() net.Conn net.Conn Close() • socket() • bind() • listen() • accept() • read() • write() • close() • socket() • connect()
  2. 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) }
  3. 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])) }
  4. 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 }
  5. 参考 -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 }
  6. 参考 -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 }
  7. コネクションプーリング ✓ コネクションを保持して再利用する ✓ 高速な通信が必要な場合 ✓ 高速化が不要であれば、都度コネクションを確立するのも一つの方法 ✓ 実装はよりシンプルに ✓

    TCPのコネクション確立はコストが高い ✓ 3ウェイハンドシェイク ✓ サーバ側のコネクション確立+α 24 // client3E is 3E frame mcp client type client3E struct { // ... // TCP connection mu sync.Mutex conn net.Conn }
  8. 並列処理される場合の考慮 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) // …
  9. エラーハンドリング 28 ✓ io.EOF や syscall.EPIPE や net.ErrClosed(Go1.16~) などのハンドリング ✓

    net.ErrClosedのおかげで“use of closed network connection”の文 字列が含まれるかどうかでチェックしなくて良くなった ✓ エラーが発生した場合に、プールしているコネクションをリリースする ✓ TCPライブラリとして提供する場合は、err != nil なときにプールしているコネク ションをリリースするのが親切 ✓ ライブラリを利用する側でエラー発生時にリリースすることもできるが
  10. 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 }
  11. リトライ 33 ✓ 瞬断はTCPの機構で救えそう ✓ (経験上)即時のリトライで救えることは少ない ✓ クライアントのリクエストが間違っている ✓ サーバが死んでいる

    ✓ ただし、コネクションプーリングしている場合は抱えているコネクションが悪く なっている場合がある ✓ アプリケーションの呼び元の責務でリトライするか判断すべき ✓ go-mcprotocol(TCPライブラリ)側では実装していない