$30 off During Our Annual Pro Sale. View Details »

実務で役立つ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. 実務で役立つTCPクライアントの作り方
    2021/04/24(Sat)
    Go Conference Online 2021 Spring
    Future Tsuji Daishiro

    View Slide

  2. Who are you?
    辻 大志郎(つじ だいしろう) @d_tutuz
    渋谷区役所(~2014/9)
    Future(2014/10~)
    ✓ 所属
    Technology Innovation Group
    競技プログラミング部
    2

    View Slide

  3. 目次
    ✓ netパッケージでTCP通信するときの基本
    ✓ 実務で使えるようなTCPクライアントを設計実装するためのポイント
    ✓ タイムアウト
    ✓ コネクションプーリング
    ✓ エラーハンドリング
    ✓ リトライ
    3

    View Slide

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

    View Slide

  5. 話さないこと
    ✓ TCPプロトコルの仕様の詳細
    ✓ ソケットシステムコールの詳細
    5

    View Slide

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

    View Slide

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

    View Slide

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

    View Slide

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

    View Slide

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

    View Slide

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

    View Slide

  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()

    View Slide

  13. どういう場合にnetパッケージを使うか
    ✓ HTTPの場合はnet/httpを使う
    ✓ 標準ライブラリでサポートされていないプロトコル実装
    ✓ MCプロトコル
    ✓ memcachedクライアント
    などなど
    13

    View Slide

  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)
    }

    View Slide

  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]))
    }

    View Slide

  16. 16
    簡単だがいくつか考慮する必要あり

    View Slide

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

    View Slide

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

    View Slide

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

    View Slide

  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)
    }

    View Slide

  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
    }

    View Slide

  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
    }

    View Slide

  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
    }

    View Slide

  24. コネクションプーリング
    ✓ コネクションを保持して再利用する
    ✓ 高速な通信が必要な場合
    ✓ 高速化が不要であれば、都度コネクションを確立するのも一つの方法
    ✓ 実装はよりシンプルに
    ✓ TCPのコネクション確立はコストが高い
    ✓ 3ウェイハンドシェイク
    ✓ サーバ側のコネクション確立+α
    24
    // client3E is 3E frame mcp client
    type client3E struct {
    // ...
    // TCP connection
    mu sync.Mutex
    conn net.Conn
    }

    View Slide

  25. 参考 -3ウェイハンドシェイク-
    ✓ コネクション確立時の通信フロー
    25
    受信側
    送信側
    SYN
    SYN ACK
    ACK
    ACK
    データ送信

    View Slide

  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)
    // …

    View Slide

  27. プールしたコネクションの状態が悪くなる場合
    27
    ✓ プールしたコネクションはクライアントが気づかない間に状態が悪くなっている可能
    性がある。
    ✓ 状態の悪いコネクションに対して読み書きするとエラーになる

    View Slide

  28. エラーハンドリング
    28
    ✓ io.EOF や syscall.EPIPE や net.ErrClosed(Go1.16~) などのハンドリング
    ✓ net.ErrClosedのおかげで“use of closed network connection”の文
    字列が含まれるかどうかでチェックしなくて良くなった
    ✓ エラーが発生した場合に、プールしているコネクションをリリースする
    ✓ TCPライブラリとして提供する場合は、err != nil なときにプールしているコネク
    ションをリリースするのが親切
    ✓ ライブラリを利用する側でエラー発生時にリリースすることもできるが

    View Slide

  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
    }

    View Slide

  30. リトライ
    30
    ✓ TCPの場合、プロトコルレイヤーで再送の機構がある
    ✓ 再送タイマー
    ✓ 重複ACK

    View Slide

  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

    View Slide

  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が複数回返ってきたことによる再送

    View Slide

  33. リトライ
    33
    ✓ 瞬断はTCPの機構で救えそう
    ✓ (経験上)即時のリトライで救えることは少ない
    ✓ クライアントのリクエストが間違っている
    ✓ サーバが死んでいる
    ✓ ただし、コネクションプーリングしている場合は抱えているコネクションが悪く
    なっている場合がある
    ✓ アプリケーションの呼び元の責務でリトライするか判断すべき
    ✓ go-mcprotocol(TCPライブラリ)側では実装していない

    View Slide

  34. リトライ方法のあれこれ
    34
    ✓ 基本はExponential Backoff+Jitter
    ✓ 接続元が限られているのであればシンプルな即時リトライも検討
    ✓ github.com/Songmu/retry など

    View Slide

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

    View Slide

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

    View Slide