Golang 从 gin 的启动到 net/http 标准库的 HTTP/1.1, HTTP/2 协议基本流程

Golang gin-gonic/gin 的启动与 net/http 标准库的 HTTP/1.1, HTTP/2 协议

本文将介绍 gin 框架的启动流程与 net/http 标准库处理 HTTP/1.1, HTTP/2 的路径关系.

一. HTTP/1.1 协议与 gin 框架的启动

对于 HTTP/1.1 协议, gin 框架的启动流程入口如下:

go 复制代码
func main() {
    r := gin.New()
    // 或 r := gin.Default(), Default() 方法本质是 gin.New() 的封装
    r.Run(":8080")
}

gin.New() 方法中, gin 框架会创建一个 gin.Engine 对象, 并通过 Engine.Run(...) 方法启动 HTTP 服务. 以下是 Engine.Run(...) 方法的实现:

go 复制代码
// github.com/gin-gonic/gin/gin.go

// "注意: 除非发生错误, 否则此方法将无限期阻塞调用的 goroutine."
func (engine *Engine) Run(addr ...string) (err error) {
    // ...
    engine.updateRouteTrees()
    address := resolveAddress(addr)
    // ...
    server := &http.Server{ // #nosec G112
        Addr:    address,
        Handler: engine.Handler(),
    }
    _ = server.ListenAndServe()
}

能够发现, gin 初始化了其路由树, 并在启动入口处构造了 http.Server 对象, 将 gin.Engine.Handler() 注入 Server.Handler, 也就是将 gin 框架的路由处理器注册到 HTTP 服务中.

二. net 标准库

在阅读 net/http 标准库之前, 需要先了解 net 标准库. net/http 的实现基于 net 标准库. 它的核心接口是 Listener 网络监听器.

go 复制代码
// net/net.go
 
// "Conn 是一个通用的面向流的网络连接.
// 多个 goroutine 可能同时调用 Conn 上的方法."
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
}

// "Listener 是一个用于面向流协议的通用网络监听器,
// 多个 goroutine 可能同时调用 Listener 上的方法."
type Listener interface {
    Accept() (Conn, error) // Accept 等待并返回监听器的下一个连接。
    Close() error
    Addr() Addr
}

Listener 接口定义了三个方法: Accept, Close, Addr. 其中 Accept 方法用于等待并返回 Conn, 即连接对象, Close 方法用于关闭监听器, Addr 方法用于获取监听器的地址信息.

已知 HTTP 是基于 TCP 连接的协议, 使用 net.Listen(...) 获取 TCP Listener 对象.

go 复制代码
// net/dial.go

// ListenConfig 包含了监听地址的配置选项。
type ListenConfig struct {
    Control func(network, address string, c syscall.RawConn) error
    KeepAlive time.Duration
    KeepAliveConfig KeepAliveConfig
    mptcpStatus mptcpStatusListen
}

// ListenConfig.Listen 会解析给定的网络和地址,并创建并返回对应网络监听器。
func (lc *ListenConfig) Listen(ctx context.Context, network, address string) (Listener, error) {
    addrs, _ := DefaultResolver.resolveAddrList(...)
    // ...
    la := addrs.first(isIPv4)
    switch la := la.(type) {
    case *TCPAddr:
        if sl.MultipathTCP() {
            l, err = sl.listenMPTCP(ctx, la)
        } else {
            l, err = sl.listenTCP(ctx, la)
        }
    case *UnixAddr: // ...
    }
    //...
}

func Listen(network, address string) (Listener, error) {
    var lc ListenConfig
    return lc.Listen(context.Background(), network, address)
}

例如使用 net.Listen("tcp", ":8080") 可以创建一个 TCPListener 对象, 监听本地的 "8080" 端口的连接请求.

三. net/http 标准库

1. Server 结构体

net/http 是基于 net 标准库构建的 HTTP 标准库.

net/http 标准库中, 最重要的结构体是 Server.

go 复制代码
// net/http/server.go
// Handler 是 HTTP 请求的处理器接口。它会处理并响应 HTTP 请求。
type Handler interface {
    ServeHTTP(ResponseWriter, *Request)
}
  
// Server 是 HTTP 服务器的实现。
type Server struct {
    Addr string
    Handler Handler
    TLSConfig *tls.Config
    // ...
    TLSNextProto map[string]func(*Server, *tls.Conn, Handler)
    // ...
    Protocols *Protocols
}

net/http 标准库中, Server 结构体是 HTTP 服务器面向对象的实现方式, 你也可以通过 http.ListenAndServe(...) 函数式的方式传入必要参数, 启动 HTTP 服务. 不过它仍然会构造 Server 结构体, 并调用对应方法启动 HTTP 服务.

2. Server 的最短启动入口

net/http 的实现中, Server 的 TLS 启动流程是基本启动流程的二次封装, 因此先介绍 TLS 启动流程, 再介绍基本启动流程.

Server 提供了两个便捷启动入口: ListenAndServeListenAndServeTLS.

go 复制代码
// net/http/server.go

func (s *Server) ListenAndServe() error {
    // ...
    ln, _ := net.Listen("tcp", addr)
    return s.Serve(ln)
}

func (s *Server) ListenAndServeTLS(certFile, keyFile string) error {
    // ...
    ln, _ := net.Listen("tcp", addr)
    return s.ServeTLS(ln, certFile, keyFile)
}

接下来深入 ServeTLS(...)Serve(...) 方法的实现, 了解 Server 的启动流程.

3. TLS 启动 (Server.ServeTLS(...)方法的实现)

带 TLS 安全层的 Server 启动, 其 TLS Listener 本质是 TCPListener + TLS Config 对二次封装 (或理解为嵌套Listener). 同理 *Server.ServeTLS(...) 也是对 *Server.Serve(...) 的二次封装.

下面是 tls.Listenertls.NewListener 的定义:

go 复制代码
// crypto/tls/tls.go

// "listener 实现了 net.Listener 接口, 用于 TLS 连接."
type listener struct {
    net.Listener
    config *Config
}

// ...
// "NewListener 创建一个 Listener,
// 它从 inner Listener 接受连接, 并使用 [Server] 包装每个连接.
// ...
func NewListener(inner net.Listener, config *Config) net.Listener {
    l := new(listener)
    l.Listener = inner
    l.config = config
    return l
}

下面是启动 TLS 服务器的入口:

go 复制代码
// net/http/server.go

// ServeTLS ...
func (s *Server) ServeTLS(l net.Listener, certFile, keyFile string) error {
    _ = s.setupHTTP2_ServeTLS()
    // config: *tls.Config
    config := cloneTLSConfig(s.TLSConfig)
    config.NextProtos = adjustNextProtos(config.NextProtos, s.protocols())
    // ...
    tlsListener := tls.NewListener(l, config)
    return s.Serve(tlsListener)
}

func (s *Server) protocols() Protocols {
    //...
    // "历史中, 禁用 HTTP/2 的方式是将 TLSNextProto 设置为非 nil 的 map,
    // 但没有 "h2" 条目."
    _, hasH2 := s.TLSNextProto["h2"]
    http2Disabled := s.TLSNextProto != nil && !hasH2
    // ...
    var p Protocols
    p.SetHTTP1(true) // default always includes HTTP/1
    if !http2Disabled { p.SetHTTP2(true) }
    return p
}

注意到 *Server.ServeTLS(...) 方法中, 通过 tls.NewListener(...) 方法将 ServeTLS(...) 方法的 Listener 对象二次封装为 TLS Listener 对象, 并传入 Serve(...) 方法中.

同时, adjustNextProtos(...) 方法, 接受 protocols() 返回的协议封装, 进行 config.NextProtos 的命名适配.

继续深入 *ServerServe(...) 方法实现, 了解启动流程.

4. 基本启动 (Server.Serve(...) 方法的实现)

go 复制代码
// net/http/server.go

// "HTTP/2 仅当 Listener 设置为 Accept() 返回 [*tls.Conn],
// 同时 TLS Listener Config.NextProtos 被设置了 "h2" 时启用."
func (s *Server) Serve(l net.Listener) error {
    // ...
    if err := s.setupHTTP2_Serve(); err != nil { return err }
    // ...
    for {
        rw, err := l.Accept()
        // ...
        c := s.newConn(rw)
        c.setState(...)
        go c.serve(...)
    }
}

可以发现服务器启动后, 在循环中监听到来的连接请求, 并通过 *Server.newConn(rw) 方法返回 *Conn 对象, 然后通过 go *Conn.serve(...) 方法处理请求.

注释中提到, HTTP/2 协议的支持需要接受 TLS Listener, 并且需要设置 Config.NextProtos 为 "h2", 这与上述 protocols()adjustNextProtos(...) 方法的功能介绍已有体现.

*Server.ServeTLS(...) 方法内部, 调用 *Server.Serve(net.Listener) 之前进行了三步主要操作:

  1. TLS 配置: setupHTTP2_ServeTLS()*Server.Serve(...), tls.NewListener(...) 前初始化 *Server.TLSConfig.
  2. adjustNextProtos(config.NextProtos, *Server.protocols()): adjustNextProtos 根据 protos 中的协议, 添加或删除 tls.Config.NextProtos 列表中的 "http/1.1" 和 "h2" 条目.
  3. tls.NewListener(l, config) *Server.Serve(tlsListener)

5. HTTP/2 协议的启动条件

观察 setupHTTP2_ServeTLS()setupHTTP2_Serve() 方法中都做了什么, 并了解 HTTP/2 协议的启动条件.

go 复制代码
// net/http/server.go

// "setupHTTP2_Serve 从 (*Server).Serve 调用,
// 并条件地使用比 setupHTTP2_ServeTLS 更保守的策略配置 s 上的 HTTP/2,
// 因为 Serve 在 tls.Listen 之后调用, 并且可能会被并发调用.
// 参见 shouldConfigureHTTP2ForServe."
func (s *Server) setupHTTP2_Serve() error {
    s.nextProtoOnce.Do(s.onceSetNextProtoDefaults_Serve)
    return s.nextProtoErr
}
func (s *Server) onceSetNextProtoDefaults_Serve() {
    if s.shouldConfigureHTTP2ForServe() {
        s.onceSetNextProtoDefaults()
    }
}
func (s *Server) setupHTTP2_ServeTLS() error {
    s.nextProtoOnce.Do(s.onceSetNextProtoDefaults)
    return s.nextProtoErr
}

setupHTTP2_ServeTLS() 方法中:

  1. 直接调用 onceSetNextProtoDefaults() 方法.

setupHTTP2_Serve() 方法中:

  1. 调用 onceSetNextProtoDefaults_Serve() 方法,
  2. 通过 shouldConfigureHTTP2ForServe() 判断后
  3. 间接调用 onceSetNextProtoDefaults() 方法.

以下是相关代码的定义:

go 复制代码
// net/http/server.go

// "shouldConfigureHTTP2ForServe 报告 Server.Serve 是否应该自动配置 HTTP/2.
// (它设置了 s.TLSNextProto map)"
func (s *Server) shouldConfigureHTTP2ForServe() bool {
    // 兼容 Go 1.6 : ...
    if s.TLSConfig == nil { return true }
    if s.protocols().UnencryptedHTTP2() { return true }

    // "用户在 http.Server 上指定了 TLSConfig.
    // 在这种情况下, 只有当他们的 tls.Config 明确提到 "h2" 时才配置 HTTP/2.
    // 否则 http2.ConfigureServer 会修改 tls.Config 来添加它,
    // 但他们可能已经将此 tls.Config 传递给 tls.NewListener.
    // 如果他们确实这样做了, 那么现在修复它也为时已晚. 它可能只是潜在的竞争条件.
    // 参见 Issue 15908."
    return slices.Contains(s.TLSConfig.NextProtos, http2NextProtoTLS)
}

// 如果用户没有另行配置(通过设置 *Server.TLSNextProto 非 nil)则配置 HTTP/2.
func (s *Server) onceSetNextProtoDefaults() {
    if omitBundledHTTP2 { return }
    p := s.protocols()
    if !p.HTTP2() && !p.UnencryptedHTTP2() { return }
    if http2server.Value() == "0" {
        http2server.IncNonDefault()
        return
    }
    if _, ok := s.TLSNextProto["h2"]; ok {
        // "TLSNextProto 已经包含了 HTTP/2 的实现.
        // 用户可能调用了 golang.org/x/net/http2.ConfigureServer 来添加它."
        return
    }
    conf := &http2Server{}
    s.nextProtoErr = http2ConfigureServer(s, conf)
}

方法主要判断两件事, 以决定是否配置 HTTP/2 协议:

  • Protocols.HTTP2()Protocols.UnencryptedHTTP2() 判断是否指定 HTTP/2 协议. (本质也是提取 *Server.TLSNextProto)
  • *Server.TLSNextProto["h2"] 判断 TLS 协议配置包含 h2, 前提是用户未自定义 *Server.TLSNextProto.

其中 TLSNextProtoServer 结构体部分定义如下:

go 复制代码
// net/http/server.go

type Server struct {
    // "TLSConfig 选择性提供一个 TLS 配置, 用于 ServeTLS 和 ListenAndServeTLS.
    // 注意, 该值会被 ServeTLS 和 ListenAndServeTLS 克隆, 因此无法使用 tls.Config.SetSessionTicketKeys 等方法修改配置.
    // 要使用 SetSessionTicketKeys, 请改用 Server.Serve 与 TLS Listener."
    TLSConfig *tls.Config
    // "TLSNextProto 可选地指定一个函数, 当 ALPN 协议升级发生时, 接管提供的 TLS 连接.
    // map 的 key 是协商的协议名称. Handler 参数应该用于处理 HTTP 请求, 并在未设置时初始化 Request 的 TLS 和 RemoteAddr. 当函数返回时, 连接会自动关闭.
    // 如果 TLSNextProto 不为 nil, 则不会自动启用 HTTP/2 支持.
    // 历史上, TLSNextProto 用于禁用 HTTP/2 支持. 现在 Server.Protocols 字段提供了更简单的方法来实现这一点."
    TLSNextProto map[string]func(*Server, *tls.Conn, Handler)
    // ...
}

进入到 http2ConfigureServer(...) 方法, 主要作用是, 初始化 *Server.TLSNextProto map, 并按照特定的键名(也就是 "h2" 和 "unencrypted_http2")将 HTTP/2 协议处理函数注册到 *Server.TLSNextProto 中.

go 复制代码
// net/http/h2_bundle.go

const http2NextProtoTLS = "h2"
const http2nextProtoUnencryptedHTTP2 = "unencrypted_http2"

func http2ConfigureServer(s *Server, conf *http2Server) error {
    // ...
    if !http2strSliceContains(s.TLSConfig.NextProtos, http2NextProtoTLS) {
        s.TLSConfig.NextProtos = append(s.TLSConfig.NextProtos, http2NextProtoTLS)
    }
    if !http2strSliceContains(s.TLSConfig.NextProtos, "http/1.1") {
        s.TLSConfig.NextProtos = append(s.TLSConfig.NextProtos, "http/1.1")
    }
    if s.TLSNextProto == nil {
        s.TLSNextProto = map[string]func(*Server, *tls.Conn, Handler){}
    }
    protoHandler := func(hs *Server, c net.Conn, h Handler, sawClientPreface bool) {
        // ...
        conf.ServeConn(c, &http2ServeConnOpts{
            Context:          ctx,
            Handler:          h,
            BaseConfig:       hs,
            SawClientPreface: sawClientPreface,
        })
    }

    s.TLSNextProto[http2NextProtoTLS] = func(hs *Server, c *tls.Conn, h Handler) {
        protoHandler(hs, c, h, false)
    }

    // "unencrypted_http2" TLSNextProto key 用于传递非 TLS 的 HTTP/2 连接.
    // 传入此方法的连接已经读取了 HTTP/2 的 preface(prior knowledge).
    s.TLSNextProto[http2nextProtoUnencryptedHTTP2] = func(
        hs *Server, c *tls.Conn, h Handler,
    ) {
        nc, err := http2unencryptedNetConnFromTLSConn(c)
        // ...
        protoHandler(hs, nc, h, true)
    }
    return nil
}

可以看出此处, http2ConfigureServer(...) 方法将 HTTP/2 协议的处理函数按照特定的键名("h2", "unencrypted_http2")注册到 s.TLSNextProto["h2"] 中, 以便在 TLS Listener 接收到 HTTP/2 连接时, 调用对应的处理函数.

综上, 以 gin 框架为例, 使用 RunTLS(...) 方法启动 HTTPS 服务器时, 配置并启动 HTTP/2 协议服务器的流程与条件如下:

  1. Engine.RunTLS(...) 方法中, 构造 http.Server 对象, 并将 gin.Engine.Handler() 注入 Server.Handler, 也就是将 gin 框架的路由处理器注册到 HTTP 服务中.

  2. 调用 Server.ListenAndServeTLS(...) 方法, 该方法内部会调用 net.Listen("tcp", addr) 创建一个 TCP Listener, 并调用 Server.ServeTLS(...) 方法启动 HTTPS 服务.

  3. Server.ServeTLS(...) 方法中, 通过 tls.NewListener(...) 方法将 ServeTLS(...) 方法的 Listener 对象二次封装为 TLS Listener 对象, 并传入 Serve(...) 方法中.

  4. Server.ServeTLS(...) 方法中, 调用 setupHTTP2_ServeTLS() 方法, 该方法会调用 onceSetNextProtoDefaults() 方法, 该方法会调用 http2ConfigureServer(...) 方法, 将 HTTP/2 协议的处理函数注册到 s.TLSNextProto["h2"] 中.

因为我们暂时不知道他会在哪里被调用, 所以我们先搁置 protoHandler(...) 注册函数的作用, 但它可以通过 *Server.TLSNextProto["h2"] 获取到, 也就是在 TLS Listener 接收到 HTTP/2 连接时, 调用对应的处理函数.

因此默认情况下(未通过 GODEBUG=http2server=0, 或设置不含 h2TLSNextProto / Protocols 显式禁用 HTTP/2), HTTP/2 协议的支持不需要开发者额外配置, 在 gin 框架下只需要传入 Engine.RunTLS(...) 方法的证书和私钥即可启动 HTTP/2 协议的 HTTPS 服务.

6. Accept() 方法

接下来应该进入 Serve(...) 方法的核心循环.

go 复制代码
// net/http/server.go

// "HTTP/2 当且仅当 Listener 设置为 Accept() 返回 [*tls.Conn],
// 同时 TLS Listener Config.NextProtos 被设置了 "h2" 时启用."
func (s *Server) Serve(l net.Listener) error {
    // ...
    if err := s.setupHTTP2_Serve(); err != nil { return err }
    // ...
    for {
        rw, err := l.Accept()
        // ...
        c := s.newConn(rw)
        c.setState(...)
        go c.serve(...)
    }
}

简单来说 Listener.Accept() 方法用于等待并返回监听器的下一个连接. Accept() 方法在有新的连接到来前是阻塞的, 会返回一个 Conn 对象.

7. serve() 方法的处理流程

Serve()ServeTLS(...), 最后都会调用 Serve() 方法, 通过 Listener.Accept() 方法获取连接请求, 创建 Conn 对象后, 通过 go Conn.serve(...) 方法处理请求.

以下是 Conn.serve(...) 方法

go 复制代码
// net/http/server.go

// Serve a new connection.
func (c *conn) serve(ctx context.Context) {
    // ...
    ctx = context.WithValue(ctx, LocalAddrContextKey, c.rwc.LocalAddr())
    // ...
    if tlsConn, ok := c.rwc.(*tls.Conn); ok {
        _ := tlsConn.HandshakeContext(ctx)
        // ...
        // NegotiateProtocol, 通常为 "h2"
        if proto := c.tlsState.NegotiatedProtocol; validNextProto(proto) {
            if fn := c.server.TLSNextProto[proto]; fn != nil {
                h := initALPNRequest{ctx, tlsConn, serverHandler{c.server}}
                // ...
                c.setState(c.rwc, StateActive, skipHooks)
                fn(c.server, tlsConn, h)
            }
            return
        }
    }

    // HTTP/1.x from here on.
    // ...
    ctx, cancelCtx := context.WithCancel(ctx)

    protos := c.server.protocols()
    if c.tlsState == nil && protos.UnencryptedHTTP2() {
        if c.maybeServeUnencryptedHTTP2(ctx) { return }
    }
    if !protos.HTTP1() { return }
    for {
        w, _ := c.readRequest(ctx)
        // ...
        serverHandler{c.server}.ServeHTTP(w, w.req)
        // ...
    }
}

Conn.serve(...) 方法中, 首先判断是否是 TLS 连接, 如果是 TLS 连接, 则进行 TLS 握手, 也就是 tlsConn.HandshakeContext(), 随后进入 ALPN 协议协商:

  • 如果协商的协议是 HTTP/2, 则使用 c.server.TLSNextProto[proto] 获取处理函数进行处理.

  • 如果判断是非加密的 HTTP/2 协议, 如果支持则调用 c.maybeServeUnencryptedHTTP2(ctx) 方法处理请求.

值得注意的是, 和 ALPN 协议协商类似, c.maybeServeUnencryptedHTTP2(ctx) 方法内, 也会使用 c.server.TLSNextProto[nextProtoUnencryptedHTTP2] 获取处理函数.

根据上面的解析, 在 http2ConfigureServer(...) 方法中, 已经将 HTTP/2 协议的处理函数注册到 s.TLSNextProto["h2"]s.TLSNextProto["unencrypted_http2"] 中.

即在 [5. HTTP/2 协议的启动条件](#5. HTTP/2 协议的启动条件 "#5-http2-%E5%8D%8F%E8%AE%AE%E7%9A%84%E5%90%AF%E5%8A%A8%E6%9D%A1%E4%BB%B6") 末尾被搁置的 protoHandler(...) 函数, 我们看看被注册的函数到底做了什么:

go 复制代码
func (s *Server) onceSetNextProtoDefaults() {
    // ...
    conf := &http2Server{}
    s.nextProtoErr = http2ConfigureServer(s, conf)
}
go 复制代码
// net/http/h2_bundle.go

func http2ConfigureServer(s *Server, conf *http2Server) error {
    // ...
    protoHandler := func(hs *Server, c net.Conn, h Handler, sawClientPreface bool) {
        if http2testHookOnConn != nil {
            http2testHookOnConn()
        }
        var ctx context.Context
        type baseContexter interface {
            BaseContext() context.Context
        }
        if bc, ok := h.(baseContexter); ok {
            ctx = bc.BaseContext()
        }
        conf.ServeConn(c, &http2ServeConnOpts{
            Context:          ctx,
            Handler:          h,
            BaseConfig:       hs,
            SawClientPreface: sawClientPreface,
        })
    }
    // ...
}

func (s *http2Server) ServeConn(c net.Conn, opts *http2ServeConnOpts) {
    if opts == nil { opts = &http2ServeConnOpts{} }
    s.serveConn(c, opts, nil)
}

func (s *http2Server) serveConn(
    c net.Conn,
    opts *http2ServeConnOpts,
    newf func(*http2serverConn),
) {
    // ...
    sc := &http2serverConn{ ... }
    sc.serve(conf)
}

func (sc *http2serverConn) serve(conf http2http2Config) {
    // ...
    settings := http2writeSettings{ ... }
    if !http2disableExtendedConnectProtocol {
        settings = append(settings, http2Setting{http2SettingEnableConnectProtocol, 1})
    }
    sc.writeFrame(http2FrameWriteRequest{ write: settings })
    // "任意的连接都从 initialWindowSize 流入令牌开始.
    // 如果配置了更高的值, 我们会添加更多的令牌."
    if diff := conf.MaxUploadBufferPerConnection - http2initialWindowSize; diff > 0 {
        sc.sendWindowUpdate(nil, int(diff)) // 更新 HTTP/2 流量控制窗口
    }
    // "readPreface 读取来自对等方的 ClientPreface 问候, 
    // 或者在超时的情况下返回 errPrefaceTimeout, 
    // 或者在问候无效时返回错误。"
    if err := sc.readPreface(); err != nil {
        sc.logf("http2: failed to read client preface from %v: %v", sc.conn.RemoteAddr(), err)
        return
    }
    // ...
    // "readFrames 是读取传入帧的循环. 它一次只读取一个帧, 直到消费者处理完该帧前阻塞."
    // 它在自己的 goroutine 中运行."
    go sc.readFrames() // closed by defer sc.conn.Close above

    // ...
    for {
        select {
        case wr := <-sc.wantWriteFrameCh:   sc.writeFrame(...)
        case res := <-sc.wroteFrameCh:      sc.wroteFrame(...)
        case res := <-sc.readFrameCh:       res.readMore()
        case m := <-sc.bodyReadCh:          sc.noteBodyRead(...)
        case msg := <-sc.serveMsgCh:
            switch v := msg.(type) {
            case *http2serverMessage: // ...
            case *http2startPushRequest: sc.startPush(...)
            case func(*http2serverConn): v(sc)
            default: panic( ... )
            }
        }
        // ...
    }
}

protoHandler(...) 方法, 主要是调用 http2Server.ServeConn(...) 方法, 该方法内部会创建 http2serverConn 对象, 并调用 http2serverConn.serve(...) 方法处理请求.

那么在 http2serverConn.serve(...) 内部主要做做三件事:

  • 协商: SETTINGS
  • 验证身份: preface
  • 拉起辅助协程: 帧读写

好吧, 在文章里纯看代码又多又复杂, 通过下面的流程图来理解 protoHandler 层层包装的 Conn.serve(...) 方法大致的处理流程.

总之 protoHandler(...) 方法, 包装了 HTTP/2 协议连接的处理流程 *http2serverConn.serve(...), 进入 HTTP/2 协议的处理流程后, 主要做三件事: 协商、验证身份、拉起辅助协程, 执行 HTTP/2 的主要处理逻辑.


以下是 HTTP/1.x 协议的处理流程.

go 复制代码
// net/http/server.go

// Serve a new connection.
func (c *conn) serve(ctx context.Context) {
    // ...
    ctx, cancelCtx := context.WithCancel(ctx)
    protos := c.server.protocols()
    if c.tlsState == nil && protos.UnencryptedHTTP2() {
        if c.maybeServeUnencryptedHTTP2(ctx) { return }
    }
    if !protos.HTTP1() { return }
    for {
        w, _ := c.readRequest(ctx)
        // ...
        serverHandler{c.server}.ServeHTTP(w, w.req)
        // ...
    }
}

服务器在处理 HTTP/1.x 的循环中, 通过 c.readRequest(ctx) 方法读取请求, 并传递至 serverHandler{c.server}.ServeHTTP(w, w.req) 方法处理.

其中 serverHandler 结构体及其 ServeHTTP 方法的定义如下:

go 复制代码
// net/http/server.go

type serverHandler struct {
    srv *Server
}

func (sh serverHandler) ServeHTTP(rw ResponseWriter, req *Request) {
    handler := sh.srv.Handler
    if handler == nil {
        handler = DefaultServeMux
    }
    if (!sh.srv.DisableGeneralOptionsHandler) &&
        (req.RequestURI == "*") &&
        (req.Method == "OPTIONS") {
            handler = globalOptionsHandler{}
    }
    handler.ServeHTTP(rw, req)
}

可知, serverHandler.ServeHTTP(...) 方法会调用 Server.Handler.ServeHTTP(...) 方法处理请求, 如果 Server.Handler 为 nil, 则使用默认的 DefaultServeMux 处理请求.

因此回头来看 gin 框架的启动流程, 在 Engine.Run(...) 方法中的:

go 复制代码
func (engine *Engine) Handler() http.Handler {
    if !engine.UseH2C {
        return engine
    }
    h2s := &http2.Server{}
    return h2c.NewHandler(engine, h2s)
}

func (engine *Engine) Run(addr ...string) (err error) {
    // ...
    engine.updateRouteTrees()
    address := resolveAddress(addr)
    // ...
    server := &http.Server{
        Addr:    address,
        Handler: engine.Handler(),
    }
    err = server.ListenAndServe()
    return
}

gin 框架在启动入口处构造了 http.Server 对象, 并将 gin.EngineHandler() 作为 Server.Handler 传入, 也就是将 gin 框架的路由处理器注册到 HTTP 服务中.

gin-gonic/gin@v1.12.0 版本为例, gin 框架的启动入口如 Run(...), RunTLS(...), RunUnix(...), 甚至 RunQuic, RunListener(...) 方法均使用 engine.Handler() 方法作为 Server.Handler 传入, 也就是将 gin 框架的路由处理器注册到 HTTP 服务中.

总之, gin 框架的启动入口方法都会构造 http.Server 对象, 并注入 gin 框架的路由处理器到 HTTP 服务器对象中, 最后通过 Server.Serve(Listener) 方法启动 HTTP 服务, 监听并处理请求.


相关推荐
logomister设计公司阿燕1 小时前
个人做商标设计注册最快多久能拿受理号?
go
妙码生花15 小时前
从 PHP 到 AI + Golang,程序员自救转型手记(五十八):后台系统配置管理实现
前端·后端·go
小满zs20 小时前
Go语言第七章(Map)
后端·go
设计总监老谢1 天前
初创公司商标设计注册拿到证要等多久?
go
用户125758524361 天前
为什么队列长度归零,不代表后台异步任务真的跑完了
redis·后端·go
他们叫我秃子2 天前
前端开发转 Go 全栈(五):终于遇到熟人了,Go 的闭包和高阶函数原来这么像 JavaScript
前端·后端·go
妙码生花2 天前
从 PHP 到 AI + Golang,程序员自救转型手记(五十七):AI时代对组件封装的新理解、agInput 统一入口,动态表单预备
前端·后端·go
妙码生花2 天前
从 PHP 到 AI + Golang,程序员自救转型手记(五十六):附件管理、增加根据文件后缀生成 SVG 文件图标的接口
前端·后端·go