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でご連絡ください。

164fd510e92a1912155b869b2c333c1e?s=128

Tsuji Daishiro

April 24, 2021
Tweet

Transcript

  1. 実務で役立つTCPクライアントの作り方 2021/04/24(Sat) Go Conference Online 2021 Spring Future Tsuji Daishiro

  2. Who are you? 辻 大志郎(つじ だいしろう) @d_tutuz 渋谷区役所(~2014/9) Future(2014/10~) ✓

    所属 Technology Innovation Group 競技プログラミング部 2
  3. 目次 ✓ netパッケージでTCP通信するときの基本 ✓ 実務で使えるようなTCPクライアントを設計実装するためのポイント ✓ タイムアウト ✓ コネクションプーリング ✓

    エラーハンドリング ✓ リトライ 3
  4. go-mcprotocol ✓ go-mcprotocol (@develop) ✓ PLCというハードウェア制御をソフトウェアで制御できる機器と通信 ✓ TCP上に、MELSECコミュニケーションプロトコル(通称:MCプロトコル)というプロトコルで通信 ✓ Go製のTCPクライアントライブラリ

    4 https://emb.macnica.co.jp/articles/4277/ PLC
  5. 話さないこと ✓ TCPプロトコルの仕様の詳細 ✓ ソケットシステムコールの詳細 5

  6. 6 netパッケージでTCP通信するときの基本

  7. netパッケージを使ったTCPの通信の基本 7 クライアント サーバー

  8. netパッケージを使ったTCPの通信の基本 8 クライアント サーバー Listen() Accept()

  9. netパッケージを使ったTCPの通信の基本 9 クライアント サーバー Listen() Accept() Dial() net.Conn net.Conn

  10. netパッケージを使ったTCPの通信の基本 10 クライアント サーバー Listen() Accept() Dial() Write() Read() Read()

    Write() net.Conn net.Conn
  11. netパッケージを使ったTCPの通信の基本 11 クライアント サーバー Listen() Accept() Dial() Write() Read() Read()

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

    Read() Write() Close() net.Conn net.Conn Close() • socket() • bind() • listen() • accept() • read() • write() • close() • socket() • connect()
  13. どういう場合にnetパッケージを使うか ✓ HTTPの場合はnet/httpを使う ✓ 標準ライブラリでサポートされていないプロトコル実装 ✓ MCプロトコル ✓ memcachedクライアント などなど

    13
  14. 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) }
  15. 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])) }
  16. 16 簡単だがいくつか考慮する必要あり

  17. 17 なお、TCPサーバ側の考慮点は 『堅牢なTCPサーバを作るために』がおすすめ https://speakerdeck.com/fujiwara3/kamakura-dot-go-number-5

  18. タイムアウトを見込む ✓ コネクションを確立できない場合 ✓ サーバからレスポンスが返ってこない場合 18 タイムアウトは、応答がなさそうだと判断したら待つの中止できるようにする単純なメカニズム うまく配置されたタイムアウトは障害の分離を可能にしてくれる。つまり、別のシステムやサブシステムやデ バイスの問題があなたの問題にならずに済む 『Release

    IT!』
  19. TCPでタイムアウトを実装しない場合 ✓ デフォルトではタイムアウトは設定されない ✓ Read()やDial()などはブロッキングAPI ✓ レスポンスは返ってこない場合、クライアントはエラーにならず、待ち続ける ✓ 運用上、生きているふりをするくらいであれば、死んだほうがまし 19

  20. 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) }
  21. 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 }
  22. 参考 -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 }
  23. 参考 -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 }
  24. コネクションプーリング ✓ コネクションを保持して再利用する ✓ 高速な通信が必要な場合 ✓ 高速化が不要であれば、都度コネクションを確立するのも一つの方法 ✓ 実装はよりシンプルに ✓

    TCPのコネクション確立はコストが高い ✓ 3ウェイハンドシェイク ✓ サーバ側のコネクション確立+α 24 // client3E is 3E frame mcp client type client3E struct { // ... // TCP connection mu sync.Mutex conn net.Conn }
  25. 参考 -3ウェイハンドシェイク- ✓ コネクション確立時の通信フロー 25 受信側 送信側 SYN SYN ACK

    ACK ACK データ送信
  26. 並列処理される場合の考慮 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) // …
  27. プールしたコネクションの状態が悪くなる場合 27 ✓ プールしたコネクションはクライアントが気づかない間に状態が悪くなっている可能 性がある。 ✓ 状態の悪いコネクションに対して読み書きするとエラーになる

  28. エラーハンドリング 28 ✓ io.EOF や syscall.EPIPE や net.ErrClosed(Go1.16~) などのハンドリング ✓

    net.ErrClosedのおかげで“use of closed network connection”の文 字列が含まれるかどうかでチェックしなくて良くなった ✓ エラーが発生した場合に、プールしているコネクションをリリースする ✓ TCPライブラリとして提供する場合は、err != nil なときにプールしているコネク ションをリリースするのが親切 ✓ ライブラリを利用する側でエラー発生時にリリースすることもできるが
  29. 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 }
  30. リトライ 30 ✓ TCPの場合、プロトコルレイヤーで再送の機構がある ✓ 再送タイマー ✓ 重複ACK

  31. 参考 -TCPの再送タイマー- 31 ✓ RTO(再送タイムアウト)による再送 https://eh-career.com/engineerhub/entry/2020/02/13/103000#%E5%86%8D%E9%80%81%E5%88%B6%E5%BE%A1

  32. 参考 -TCPの重複ACK- 32 https://eh-career.com/engineerhub/entry/2020/02/13/103000#%E5%86%8D%E9%80%81%E5%88%B6%E5%BE%A1 ✓ 同じシーケンス番号を要求するACKが複数回返ってきたことによる再送

  33. リトライ 33 ✓ 瞬断はTCPの機構で救えそう ✓ (経験上)即時のリトライで救えることは少ない ✓ クライアントのリクエストが間違っている ✓ サーバが死んでいる

    ✓ ただし、コネクションプーリングしている場合は抱えているコネクションが悪く なっている場合がある ✓ アプリケーションの呼び元の責務でリトライするか判断すべき ✓ go-mcprotocol(TCPライブラリ)側では実装していない
  34. リトライ方法のあれこれ 34 ✓ 基本はExponential Backoff+Jitter ✓ 接続元が限られているのであればシンプルな即時リトライも検討 ✓ github.com/Songmu/retry など

  35. 参考 -TCPキープアライブ- 35 ✓ TCPキープアライブ ✓ 確立したコネクションの死活を確認し、通信できない場合はコネクションをク ローズ ✓ TCPでnet.Dial()を行った場合はデフォルトでTCPキープアライブが有効

    (Go1.12~
  36. まとめ 36 ✓ GoでTCPクライアントは簡単に実装できる ✓ TCPクライアントを実装するときに以下の考慮ポイントがある ✓ タイムアウト ✓ コネクションプーリング

    ✓ エラーハンドリング ✓ リトライ