Golang HTTP 标准库实现原理(上) | 青训营
2023/8/21 ·雨辰login
又过了很久,决定再更新几篇笔记。这篇主要来讲讲我之前几乎没有涉及过的领域:HTTP。
本篇内容引用自知乎用户@小徐先生.
这真的是一个宝藏博主,B站也有号:小徐先生1212
所以把他的笔记找来跟大家分享,希望大家都去看看,真的做的非常好。
1 整体框架
1.1 C-S架构
http 协议下,交互框架是由客户端(Client)和服务端(Server)两个模块组成的 C-S 架构,两个部分正好对应为本文研究的两条主线.
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 的主干链路,避免晕车.
在 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
调用 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)
}
接下来,兜兜转转依次调用 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 小节中模糊匹配问题的伏笔回收.