Goのネットワークプログラミングでよくあるエラーのbroken pipeを深く理解するために

Goでネットワークアプリケーションを開発する際、時々「broken pipe」というエラーに遭遇することがあるでしょう。この記事では、このエラーの意味を説明して、再現するためのコードを示して、基本的な対処法をいくつか紹介します。

「broken pipe」は通常、**（通信している同士の一方が）相手がリセットした（RST）TCPコネクション**にデータを書き込もうとしたときに発生する。このエラーは以下のコードで再現できる。

```go
// ...
var done = make(chan struct{})

func server() {
    listener, err := net.Listen("tcp", ":8080")
    if err != nil {
        log.Fatal(err)
    }
    defer listener.Close()

    done <- struct{}{}
    conn, err := listener.Accept()
    if err != nil {
        log.Fatal("Server Accept", err)
        os.Exit(1)
    }
    log.Println("Server: Accepted one connection")
  
    data := make([]byte, 1)                          // ╮
    if _, err := conn.Read(data); err != nil {       // |
        log.Fatal("Server Read", err)                // |
    }                                                // ├ ①
                                                     // |
    log.Printf("Server: Read %v, Closing\n", data)   // |
    conn.Close()                                     // ╯
}
```

`server()`によって、**1バイト読み込んでからTCPコネクションを閉じる**①サーバをシミュレートする。

```go
func client() {
    conn, err := net.Dial("tcp", "localhost:8080")
    if err != nil {
        log.Fatal("Client Dial", err)
    }

    // write to make the connection closed on the server side
    log.Println("Client:", time.Now(), "writes 1st byte 'a' to server")
    if _, err := conn.Write([]byte("a")); err != nil {
        log.Printf("Client: %v", err)
    }
    time.Sleep(5 * time.Second)

    // write to generate an RST packet
    log.Println("Client:", time.Now(), "writes 2nd byte 'b' to server")
    if _, err := conn.Write([]byte("b")); err != nil { // ②
        log.Printf("Client: %v", err)
    }
    time.Sleep(5 * time.Second)

    // write to generate the broken pipe error
    log.Println("Client:", time.Now(), "writes 3rd byte 'c' to server")
    if _, err := conn.Write([]byte("c")); err != nil { // ③
        log.Printf("Client: %v", err)
    }
}

func main() {
    go server()
    <-done

    client()
}
```

次に、`client()`で、サーバとのTCPコネクションを確立し、a、b、cという3バイトを5秒おきに、一つずつ送信するクライアントをシミュレートする。

サーバはクライアントからの最初のバイトaを受信した後、直ちにコネクションを閉じる。

興味深いのは、クライアントは2番目のバイトbを送信した時点（5秒後）で「broken pipe」に遭遇するのか、それとも3番目のバイトcを送信した時点（10秒後）で「broken pipe」に遭遇するのかということである。

実行の結果は以下のようになる：

```bash
$ go run broken_pipe.go
Server: Accepted one connection
Client: 2024-07-25 12:34:34.398414 +0800 CST m=+0.006754554 writes 1st byte 'a' to server
Server: Read [97], Closing
Client: 2024-07-25 12:34:39.400916 +0800 CST m=+5.009099302 writes 2nd byte 'b' to server
Client: 2024-07-25 12:34:44.401268 +0800 CST m=+10.009341019 writes 3rd byte 'c' to server
Client: write tcp 127.0.0.1:54992->127.0.0.1:8080: write: broken pipe
```

結果からして、5秒後、サーバはすでにTCPコネクションを閉じているにもかかわらず、クライアントは2番目のバイトのbの送信が`conn.Write([]byte("b"))` ②「broken pipe」エラーをもたらさないだけではなく、別のエラーも発生していないということが分かる。

`tcpdump`のパケットキャプチャの結果を確認すると、クライアントはサーバにbを送信した後（8行目の`PSH, ACK`）、サーバは`ACK`で応答せず、TCPコネクションをリセットするための`RST`を返す（9行目）。これは、サーバがその前に `FIN, ACK` (6行目) でコネクションを閉じたためである。

![img01-tcpdump-broken_pipe](https://p.ipic.vip/qtu2fa.png)

さらに5秒後、クライアントは3番目のバイトのcをサーバに送ると、コネクションがすでにサーバにリセットされているので、「broken pipe」が発生する③。言い換えれば、**相手側によって閉じられた（FIN）コネクションに一度データを書き込んでも、「broken pipe」にはならない**。

では、どのように「broken pipe」に対処すればよいかというと、次のような方法がある。

* TCPコネクションの状態をチェックしておく

データを送信する前にコネクションの状態をチェックし、まだ接続していることを確認する。

特にコネクション・プーリングを使用している場合、プールが許容するアイドル・タイムアウトは、ピアが許容するライブ時間よりも大きい場合、プールから取得したコネクションはピアによって閉じられている可能性が高いため、Test on Borrow のポリシーを使用して、コネクションのステータスをチェックし、有効なコネクションではないかどうかを確認してから、データを送受信する必要がある。

* 適切にコネクションを閉じる

コネクションが不要になったら、早めに適切な方法でコネクションを閉じる。

* エラー処理と再試行のメカニズム

接続の信頼性を確保するために、接続エラーが発生したときに再試行のポリシーを実装する。

---
「broken pipe」というエラーは、ネットワーク・プログラミングではよくある問題であり、適切なエラー処理と接続管理によって通常は回避できます。このエラーの原因と対処法を理解することは、堅牢なネットワーク・アプリケーションを構築する上で非常に重要であると思っています。