Golang HTTP 标准库实现原理(上) | 青训营

317 阅读6分钟

Golang HTTP 标准库实现原理(上) | 青训营

2023/8/21 ·雨辰login

又过了很久,决定再更新几篇笔记。这篇主要来讲讲我之前几乎没有涉及过的领域:HTTP。

本篇内容引用自知乎用户@小徐先生.

这真的是一个宝藏博主,B站也有号:小徐先生1212

所以把他的笔记找来跟大家分享,希望大家都去看看,真的做的非常好。

1 整体框架

1.1 C-S架构

http 协议下,交互框架是由客户端(Client)和服务端(Server)两个模块组成的 C-S 架构,两个部分正好对应为本文研究的两条主线.

image.png

1.2 启动 http 服务

在 Golang 启动一个 http 服务只需寥寥数笔,非常方便,代码示例如下:

import (

"net/http"

)

func main() {

http.HandleFunc("/ping", func(w http.ResponseWriter, r *http.Request) {*

w\.Write(\[]byte("pong"))

})

http.ListenAndServe(":8091", nil)

}

在上述代码中,完成了两件事:

  • 调用 http.HandleFunc 方法,注册了对应于请求路径 /ping 的 handler 函数
  • 调用 http.ListenAndServe,启动了一个端口为 8091 的 http 服务

如此简洁轻便即实现了一个 http server 的启动,其背后究竟隐藏了哪些实施细节呢. 这个问题,就让我们在第 2 章的内容中展开探讨。

1.3 发送 http 请求

在 Golang 中发送 http 请求的实现同样非常简单. 下面给出一例发送 JSON POST 请求的代码示例.

func main() {

reqBody, *:= json.Marshal(map\[string]string{"key1": "val1", "key2": "val2"})*

resp, *:= http.Post(":8091", "application/json", bytes.NewReader(reqBody))*

defer resp.Body.Close()

respBody, *:= io.ReadAll(resp.Body)*

fmt.Printf("resp: %s", respBody)

}

这部分将作为本文的第二条线索,放在第 3 章中展开讨论.

1.4 源码位置一览

本文涉及内容的源码均位于 net/http 库下,各模块的文件位置如下表所示:

模块文件服务端net/http/server.go客户端——主流程net/http/client.go客户端——构造请求net/http/request.go客户端——网络交互net/http/transport.go

2 服务端

2.1 核心数据结构

首先对 http 服务端模块涉及的核心数据结构作简要介绍.

(1)Server

基于面向对象的思想,整个 http 服务端模块被封装在 Server 类当中.

Handler 是 Server 中最核心的成员字段,实现了从请求路径 path 到具体处理方法 handler 的注册和映射能力.

在用户构造 Server 对象时,倘若其中的 Handler 字段未显式声明,则会取 net/http 包下的单例对象 DefaultServeMux(ServerMux 类型) 进行兜底.

type Server struct {

// server 的地址

Addr string

// 路由器.

Handler Handler // handler to invoke, http.DefaultServeMux if nil

// ...

}

(2)Handler

Handler 是一个 interface,暴露了方法: ServeHTTP.

该方法的作用是,根据 http 请求 Request 中的请求路径 path 映射到对应的 handler 处理函数,对请求进行处理和响应.

type Handler interface {

ServeHTTP(ResponseWriter, *Request)*

}

(3)ServeMux

ServeMux 是对 Handler 的具体实现,内部通过一个 map 维护了从 path 到 handler 的映射关系.

type ServeMux struct {

mu sync.RWMutex

m map\[string]muxEntry

es \[]muxEntry // slice of entries sorted from longest to shortest.

hosts bool // whether any patterns contain hostnames

}

(4)muxEntry

muxEntry 为一个 handler 单元,内部包含了请求路径 path + 处理函数 handler 两部分.

type muxEntry struct {

h Handler

pattern string

}

2.2 注册 handler

首先给出服务端注册 handler 的主干链路,避免晕车.

image.png

在 net/http 包下声明了一个单例 ServeMux,当用户直接通过公开方法 http.HandleFunc 注册 handler 时,则会将其注册到 DefaultServeMux 当中.

var DefaultServeMux = &defaultServeMux

var defaultServeMux ServeMux

func HandleFunc(pattern string, handler func(ResponseWriter, *Request)) {

DefaultServeMux.HandleFunc(pattern, handler)

}

在 ServeMux.HandleFunc 内部会将处理函数 handler 转为实现了 ServeHTTP 方法的 HandlerFunc 类型,将其作为 Handler interface 的实现类注册到 ServeMux 的路由 map 当中.

type HandlerFunc func(ResponseWriter, *Request)

// ServeHTTP calls f(w, r).

func (f HandlerFunc) ServeHTTP(w ResponseWriter, r *Request) {*

f(w, r)

}

func (mux *ServeMux) HandleFunc(pattern string, handler func(ResponseWriter,* Request)) {

// ...

mux.Handle(pattern, HandlerFunc(handler))

}

实现路由注册的核心逻辑位于 ServeMux.Handle 方法中,两个核心逻辑值得一提:

  • 将 path 和 handler 包装成一个 muxEntry,以 path 为 key 注册到路由 map ServeMux.m 中
  • 响应模糊匹配机制. 对于以 '/' 结尾的 path,根据 path 长度将 muxEntry 有序插入到数组 ServeMux.es 中.(模糊匹配机制的伏笔在 2.3 小节回收)
func (mux *ServeMux) Handle(pattern string, handler Handler) {

mux.mu.Lock()

defer mux.mu.Unlock()

// ...

e := muxEntry{h: handler, pattern: pattern}

mux.m\[pattern] = e

if pattern\[len(pattern)-1] == '/' {

mux.es = appendSorted(mux.es, e)

}

// ...

}
func appendSorted(es []muxEntry, e muxEntry) []muxEntry {

n := len(es)

i := sort.Search(n, func(i int) bool {

return len(es\[i].pattern) < len(e.pattern)

})

if i == n {

return append(es, e)

}

es = append(es, muxEntry{}) // try to grow the slice in place, any entry works.

copy(es\[i+1:], es\[i:]) // Move shorter entries down

es\[i] = e

return es

}

2.3 启动 server

image.png 调用 net/http 包下的公开方法 ListenAndServe,可以实现对服务端的一键启动. 内部会声明一个新的 Server 对象,嵌套执行 Server.ListenAndServe 方法.

func ListenAndServe(addr string, handler Handler) error {

server := \&Server{Addr: addr, Handler: handler}

return server.ListenAndServe()

}

Server.ListenAndServe 方法中,根据用户传入的端口,申请到一个监听器 listener,继而调用 Server.Serve 方法.

func (srv *Server) ListenAndServe() error {

// ...

addr := srv.Addr

if addr == "" {

addr = ":http"

}

ln, err := net.Listen("tcp", addr)

// ...

return srv.Serve(ln)

}

Server.Serve 方法很核心,体现了 http 服务端的运行架构:for + listener.accept 模式.

  • 将 server 封装成一组 kv 对,添加到 context 当中
  • 开启 for 循环,每轮循环调用 Listener.Accept 方法阻塞等待新连接到达
  • 每有一个连接到达,创建一个 goroutine 异步执行 conn.serve 方法负责处理
var ServerContextKey = &contextKey{"http-server"}

type contextKey struct {

name string

}

func (srv *Server) Serve(l net.Listener) error {*

// ...

ctx := context.WithValue(baseCtx, ServerContextKey, srv)

for {

rw, err := l.Accept()

// ...

connCtx := ctx

// ...

c := srv.newConn(rw)

// ...

go c.serve(connCtx)

}

}

conn.serve 是响应客户端连接的核心方法:

  • 从 conn 中读取到封装到 response 结构体,以及请求参数 http.Request
  • 调用 serveHandler.ServeHTTP 方法,根据请求的 path 为其分配 handler
  • 通过特定 handler 处理并响应请求
func (c *conn) serve(ctx context.Context) {

// ...

c.r = \&connReader{conn: c}

c.bufr = newBufioReader(c.r)

c.bufw = newBufioWriterSize(checkConnErrorWriter{c}, 4<<10)

for {

w, err := c.readRequest(ctx)

// ...

serverHandler{c.server}.ServeHTTP(w, w\.req)

w\.cancelCtx()

// ...

}

}

在 serveHandler.ServeHTTP 方法中,会对 Handler 作判断,倘若其未声明,则取全局单例 DefaultServeMux 进行路由匹配,呼应了 http.HandleFunc 中的处理细节.

func (sh serverHandler) ServeHTTP(rw ResponseWriter, req *Request) {

handler := sh.srv.Handler

if handler == nil {

handler = DefaultServeMux

}

// ...

handler.ServeHTTP(rw, req)

}

image.png 接下来,兜兜转转依次调用 ServeMux.ServeHTTP、ServeMux.Handler、ServeMux.handler 等方法,最终在 ServeMux.match 方法中,以 Request 中的 path 为 pattern,在路由字典 Server.m 中匹配 handler,最后调用 handler.ServeHTTP 方法进行请求的处理和响应.

func (mux *ServeMux) ServeHTTP(w ResponseWriter, r *Request) {

// ...

h, *:= mux.Handler(r)*

h.ServeHTTP(w, r)

}
func (mux *ServeMux) Handler(r *Request) (h Handler, pattern string) {

// ...

return mux.handler(host, r.URL.Path)

}
func (mux *ServeMux) handler(host, path string) (h Handler, pattern string) {

mux.mu.RLock()

defer mux.mu.RUnlock()

// ...

h, pattern = mux.match(path)

// ...

return

}

值得一提的是,当通过路由字典 Server.m 未命中 handler 时,此时会启动模糊匹配模式,两个核心规则如下:

  • 以 '/' 结尾的 pattern 才能被添加到 Server.es 数组中,才有资格参与模糊匹配
  • 模糊匹配时,会找到一个与请求路径 path 前缀完全匹配且长度最长的 pattern,其对应的handler 会作为本次请求的处理函数.
func (mux *ServeMux) match(path string) (h Handler, pattern string) {
    v, ok := mux.m[path]
    if ok {
        return v.h, v.pattern
    }

    // ServeMux.es 本身是按照 pattern 的长度由大到小排列的
    for _, e := range mux.es {
        if strings.HasPrefix(path, e.pattern) {
            return e.h, e.pattern
        }
    }
    return nil, ""
}

至此,2.2 小节中模糊匹配问题的伏笔回收.