likes
comments
collection
share

深入理解 Go 网络编程中的常见错误之一——broken pipe

作者站长头像
站长
· 阅读数 25

使用 Go 编写网络应用程序时,大家应该多多少少都遇到过“broken pipe”这个错误吧。本文将探讨这一错误的含义、给出能复现的代码片段、并列举一些基本的应对方法。

“broken pipe”错误通常发生在(通信双方中的一方)尝试向由对端关闭的连接写入数据时。可以通过如下的代码来重现这种错误:

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()                                     // ╯
}

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

执行结果如下:

$ 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

我们通过 server() 函数模拟了一个读取 1 个字节后就关闭连接的服务器client() 函数模拟的客户端与该服务器建立连接后,会先后发送 a、b、c 这 3 个字节,发送间隔为 5 秒。服务器收到来自客户端的第 1 个字节 a 后,就会立即关闭连接①。

接下来就是比较有意思的地方了,客户端是在(5 秒后)发送第 2 个字节 b 时产生了“broken pipe”错误,还是在(10 秒后)发送第 3 个字节 c 时会产生这个错误呢?

从执行结果来看,5 秒之后,客户端又向服务器发送了第 2 个字节 b,尽管此时服务器已经关闭了连接,但 conn.Write([]byte("b")) ②并不会返回“broken pipe”的错误,甚至没有返回任何错误。

结合 tcpdump 抓包的结果来看,

深入理解 Go 网络编程中的常见错误之一——broken pipe

客户端向服务器发送了 b 之后(第 8 行,PSH, ACK),服务器没有像第 5 行那样正常响应 ACK,而是返回 RST 重置了连接(第 9 行)。这是因为服务器此前已经通过 FIN, ACK 表明要关闭连接了(第 6 行)。

又过了 5 秒,当客户端再次向服务器发送第 3 个字节 c 时,(已接收到 RST 的连接)才会返回“broken pipe”的错误③。也就是说,第一次向由对端关闭的连接写入数据并不会产生“broken pipe”

那么,该如何处理“broken pipe”呢,我们可以采取以下几种策略:

  1. 检查连接状态:在发送数据之前,始终检查连接的状态,确保连接仍处于打开状态

特别是在使用连接池时,若连接池允许的连接最大空闲时间(idle timeout)大于对端允许的连接生存时间,那么从池中获取的连接很可能已被对端关闭。此时应使用 Test on Borrow 策略先检查连接的状态,保证从连接池中获取的是真正有效的连接,再在上面收发数据

  1. 优雅地关闭连接:当不再需要连接时,始终使用适当的方法关闭连接,避免出现意外的连接关闭
  2. 错误处理和重试机制:在发生连接错误时,实现重试连接的策略,以确保连接的可靠性

"broken pipe" 错误是网络编程中的常见问题,通常可以通过适当的错误处理和连接管理来避免。理解这一错误的原因和处理方法对于构建稳健的网络应用程序至关重要。

转载自:https://juejin.cn/post/7395147410211880969
评论
请登录