gin源码-499错误

未匹配的标注

http请求时,如果客户端主动断开连接,服务端一般会产生一条499请求的错误日志,具体处理流程是什么样的呢,先写个简单的demo如下。启动服务后,发起一个请求然后快速取消掉,随后5秒后gin打印499日志,整个调用链路可以看这里

package main

import (
    "github.com/gin-gonic/gin"
    "time"
)

func main() {
    r := gin.Default()
    r.GET("/", func(c *gin.Context) {
        time.Sleep(time.Second * 5)
        select {
        case <-c.Request.Context().Done():
            c.String(499, "Timeout")
        default:
            c.String(200, "Hello World")
        }
    })
    _ = r.Run(":8000")
}

在上一节中我们还忽略了一个协程goroutine-18 created by net/http.(*connReader).startBackgroundRead.func2这个协程是跟随每个http请求而创建的,那么它的作用是什么呢,结合这个我们来看本次demo的调用链:

|-goroutine-50 created by net/http.(*Server).Serve.func3
  |-net/http.(*conn).serve
    |-net/http.(*conn).readRequest
    |-net/http.(*connReader).startBackgroundRead()     // 这里会启动goroutine-7
    |-net/http.serverHandler.ServeHTTP
      |-github.com/gin-gonic/gin.(*Engine).ServeHTTP
        |-github.com/gin-gonic/gin.(*Engine).handleHTTPRequest
          |-github.com/gin-gonic/gin.LoggerWithConfig.func1
            |-github.com/gin-gonic/gin.CustomRecoveryWithWriter.func1
              |-main.main.func1
                |-context.(*cancelCtx).Done           // 收到goroutine-7的cancelCtx
        |-sync.(*Pool).Put
    |-net/http.(*response).finishRequest

|-goroutine-7 created by net/http.(*connReader).startBackgroundRead.func2
  |-net/http.(*connReader).backgroundRead
    |-net.(*TCPConn).Read                      // 此时http的请求已经读完,继续读取1字节
    |-net/http.(*connReader).lock
    |-net/http.(*connReader).handleReadError   // 如果读到一个错误(例如客户端主动关闭连接)
      |-context.WithCancel.func1
        |-context.(*cancelCtx).cancel          // 调用cr.conn.cancelCtx()
    |-sync.(*Cond).Broadcast

可以看到goroutine-7的startBackgroundRead主要作用就是检测当前tcp连接是否alive,如果已经挂了就应该通知业务协程。因为http1.x是有keep-alive机制的,也就是一个连接可以服务多个http请求,疑惑startBackgroundRead协程生命周期是跟随连接还是http请求。继续写一个如下的demo来验证,得到的请求链路看这里

func main() {
    var s = http.Server{
        Addr: ":8000",
        Handler: http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
            fmt.Println(r.RemoteAddr)
            w.Write([]byte("Hello World"))
        }),
    }
    go func() {
        time.Sleep(time.Second * 5)
        c := &http.Client{}
        req, err := http.NewRequest("GET", "http://localhost:8000", nil)
        if err != nil {
            panic(err)
        }
        for i := 0; i < 5; i++ {
            resp, _ := c.Do(req)
            io.ReadAll(resp.Body)
        }
    }()
    s.ListenAndServe()
}

可以看到,发起了五次http请求,都是共用的同一个连接,但是startBackgroundRead协程有五个。这些请求都是正常的http请求,请求结束后,它是如何通知startBackgroundRead协程退出的呢?关键调用链路如下:

|-goroutine-36 created by net/http.(*Server).Serve.func3
  |-net/http.(*conn).serve
    |-net/http.(*conn).readRequest
    |-net/http.(*connReader).startBackgroundRead()     // 这里会启动goroutine-20
    |-net/http.serverHandler.ServeHTTP
    |-net/http.(*response).finishRequest
      |-net/http.(*connReader).abortPendingRead
        |-net/http.(*connReader).lock
        |-net.(*TCPConn).SetReadDeadline(aLongTimeAgo) // 触发一个timeout错误
        |-sync.(*Cond).Wait                            // 等待只到goroutine-20收到
        |-net.(*TCPConn).SetReadDeadline(time.Time{})  // 设置读超时为0(不限)
        |-net/http.(*connReader).unlock
      |-net/http.(*noBody).Close

|-goroutine-20 created by net/http.(*connReader).startBackgroundRead.func2
  |-net/http.(*connReader).backgroundRead
    |-net.(*TCPConn).Read                              // 读到timeout错误,退出...
    |-net/http.(*connReader).lock
    |-net.(*OpError).Timeout                           
    |-sync.(*Cond).Broadcast

本文章首发在 LearnKu.com 网站上。

上一篇 下一篇
讨论数量: 0
发起讨论 只看当前版本


暂无话题~