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