Slide 1

Slide 1 text

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

Slide 2

Slide 2 text

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

Slide 3

Slide 3 text

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

Slide 4

Slide 4 text

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

Slide 5

Slide 5 text

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

Slide 6

Slide 6 text

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

Slide 7

Slide 7 text

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

Slide 8

Slide 8 text

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

Slide 9

Slide 9 text

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

Slide 10

Slide 10 text

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

Slide 11

Slide 11 text

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

Slide 12

Slide 12 text

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

Slide 13

Slide 13 text

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

Slide 14

Slide 14 text

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

Slide 15

Slide 15 text

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

Slide 16

Slide 16 text

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

Slide 17

Slide 17 text

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

Slide 18

Slide 18 text

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

Slide 19

Slide 19 text

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

Slide 20

Slide 20 text

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

Slide 21

Slide 21 text

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 }

Slide 22

Slide 22 text

参考 -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 }

Slide 23

Slide 23 text

参考 -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 }

Slide 24

Slide 24 text

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

Slide 25

Slide 25 text

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

Slide 26

Slide 26 text

並列処理される場合の考慮 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) // …

Slide 27

Slide 27 text

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

Slide 28

Slide 28 text

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

Slide 29

Slide 29 text

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 }

Slide 30

Slide 30 text

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

Slide 31

Slide 31 text

参考 -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

Slide 32

Slide 32 text

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

Slide 33

Slide 33 text

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

Slide 34

Slide 34 text

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

Slide 35

Slide 35 text

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

Slide 36

Slide 36 text

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