本文基于 go version go1.23.3 darwin/arm64 来对Golang中的 HTTP 标准库实现原理进行深入解读。
整体框架
在 Golang 当中,启动一个 http 服务非常方便:
1 | import ( |
在上述代码中,做了两件事:
- 调用
http.HandleFunc
方法,注册了对应请求路径 /ping 的 handler。 - 调用
http.ListenAndServe
方法,启动了一个端口为 8091 的 http 服务。
在 Golang 当中发送 http 请求的实现也同样简单。例如:
1 | func main() { |
本文涉及内容的源码均位于 net/http 库下,各模块的文件位置如下表所示:
模块 | 文件 |
---|---|
服务端 | net/http/server.go |
客户端——主流程 | net/http/client.go |
客户端——构造请求 | net/http/request.go |
客户端——网络交互 | net/http/transport.go |
服务端
核心数据结构
Server
基于面向对象的思想,整个 http 服务端模块都被封装在 Server 类当中。
1 | type Server struct { |
Handler 是 Server 中最核心的字段,实现了从请求路径 path 到具体处理函数 handler 的注册和映射能力。
Handler
1 | type Handler interface { |
Handler 是一个 interface,定义了方法:ServeHTTP。
该方法的作用是,根据 http 请求 Request 中的请求路径 path 映射到对应的 handler 处理函数,对请求进行处理和响应。
pattern
1 | type pattern struct { |
pattern 用于表示一个可以被 HTTP 请求匹配的 URL 模式,包含了 optional method、optional host 和 path。
segment
1 | type segment struct { |
segment 用于定义如何匹配 pattern 中的一个段或多个段,同时支持特殊语法(如通配符和尾部斜杠)
ServeMux
1 | type ServeMux struct { |
ServeMux 是对 Handler 的具体实现。在 patterns
字段中存储了所有注册的路由(可能会在未来的版本中被移除),并采用了树形结构对路由节点进行存储。
routingNode
1 | type routingNode struct { |
routingNode 是用于实现路由决策树的核心。它既可以表示叶子节点,也可以表示内部节点。
routingIndex
1 | type routingIndex struct { |
routingIndex 是用于优化路由冲突检测的索引结构,它的核心思想在于将 pattern 划分为 segments,然后通过将相同位置的 segment 进行比较以快速排除掉不可能冲突的 pattern。
注册handler
当用户调用公开方法 http.HandleFunc 注册 handler 时,其实会将其注册到一个 ServeMux 类型的单例 DefaultServeMux 对象当中。
1 | // HandleFunc registers the handler function for the given pattern in [DefaultServeMux]. |
1 | // DefaultServeMux is the default [ServeMux] used by [Serve]. |
在 ServeMux.register 方法中,只是简单调用了 ServeMux.registerErr 方法并处理抛出的异常,具体的注册逻辑都是在 ServeMux.registerErr 方法中实现的。
1 | func (mux *ServeMux) register(pattern string, handler Handler) { |
可以看到,在 ServeMux.registerErr 方法中,有几个核心逻辑:
- 解析 pattern 字符串
- 获取注册 pattern 所在的源代码位置
- 检测当前 pattern 与已注册的 patterns 是否存在冲突
- 添加当前 pattern
接下来,我们来逐个看看具体的方法实现:可以看到,parsePattern 方法中做的事情其实就是将 patternStr 转换为 pattern 对象,并且根据 “/“ 划分为一个个的 segments,其中兼容了 “{name}”, “{name…}”, 和 “{$}” 这几种通配符的特殊情况。1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112func parsePattern(s string) (_ *pattern, err error) {
// 判空
if len(s) == 0 {
return nil, errors.New("empty pattern")
}
off := 0 // offset into string
// 异常处理
defer func() {
if err != nil {
err = fmt.Errorf("at offset %d: %w", off, err)
}
}()
// pattern的格式为[METHOD] [HOST]/[PATH]
// 在此处根据空格或\t分割为左右两部分
// found表示pattern中是否存在METHOD
method, rest, found := s, "", false
if i := strings.IndexAny(s, " \t"); i >= 0 {
method, rest, found = s[:i], strings.TrimLeft(s[i+1:], " \t"), true
}
if !found {
rest = method
method = ""
}
if method != "" && !validMethod(method) {
return nil, fmt.Errorf("invalid method %q", method)
}
p := &pattern{str: s, method: method}
if found {
off = len(method) + 1
}
// 根据"/"分割为host和path两部分
i := strings.IndexByte(rest, '/')
if i < 0 {
return nil, errors.New("host/path missing /")
}
p.host = rest[:i]
rest = rest[i:]
// 检测host段中是否存在"{"
// 因为path中存在类似"{name}", "{name...}", or "{$}"这样的特殊匹配
// 所以如果host段中出现"{"符号的话,就可以认为出现了将path划分到了host段这样的异常情况
if j := strings.IndexByte(p.host, '{'); j >= 0 {
off += j
return nil, errors.New("host contains '{' (missing initial '/'?)")
}
// At this point, rest is the path.
off += i
// An unclean path with a method that is not CONNECT can never match,
// because paths are cleaned before matching.
// 这里的clean path指的是规范的path,不存在"."或".."这样的元素
if method != "" && method != "CONNECT" && rest != cleanPath(rest) {
return nil, errors.New("non-CONNECT pattern with unclean path can never match")
}
seenNames := map[string]bool{} // remember wildcard names to catch dups
for len(rest) > 0 {
// Invariant: rest[0] == '/'.
rest = rest[1:]
off = len(s) - len(rest)
// 匹配最后rest=="/"的情况
if len(rest) == 0 {
// Trailing slash.
p.segments = append(p.segments, segment{wild: true, multi: true})
break
}
i := strings.IndexByte(rest, '/')
if i < 0 {
i = len(rest)
}
var seg string
seg, rest = rest[:i], rest[i:]
if i := strings.IndexByte(seg, '{'); i < 0 {
// Literal.
seg = pathUnescape(seg)
p.segments = append(p.segments, segment{s: seg})
} else {
// Wildcard.
if i != 0 {
return nil, errors.New("bad wildcard segment (must start with '{')")
}
if seg[len(seg)-1] != '}' {
return nil, errors.New("bad wildcard segment (must end with '}')")
}
name := seg[1 : len(seg)-1]
if name == "$" {
if len(rest) != 0 {
return nil, errors.New("{$} not at end")
}
p.segments = append(p.segments, segment{s: "/"})
break
}
name, multi := strings.CutSuffix(name, "...")
if multi && len(rest) != 0 {
return nil, errors.New("{...} wildcard not at end")
}
if name == "" {
return nil, errors.New("empty wildcard")
}
if !isValidWildcardName(name) {
return nil, fmt.Errorf("bad wildcard name % q", name)
}
if seenNames[name] {
return nil, fmt.Errorf("duplicate wildcard name %q", name)
}
seenNames[name] = true
p.segments = append(p.segments, segment{s: name, wild: true, multi: multi})
}
}
return p, nil
}
1 | func (idx *routingIndex) possiblyConflictingPatterns(pat *pattern, f func(*pattern) error) (err error) { |
简单来说,在 possiblyConflictingPatterns 方法中,可以快速找出所有可能与给定路由模式 pat 冲突的已注册模式。它的核心思想是通过预置的索引(segments 和 multis)缩小检查范围,避免遍历所有模式,从而提升性能。这里传入的 f 函数为一个匿名函数:
1 | func(pat2 *pattern) error { |
在 pat.conflictsWith 方法中进行了 pattern 是否存在冲突的比较:
1 | func (p1 *pattern) conflictsWith(p2 *pattern) bool { |
这里的逻辑就比较简单,先比较 host 是否相同,然后比较 path 和 method 是否相同来判断两个 pattern 之间是否存在冲突。
1 | // addPattern adds a pattern and its associated Handler to the tree |
routingNode.addPattern 方法就是将 pattern 组织成树状结构,并将 pattern 和对应的 handler 信息保存在叶子结点上:
1 | func (idx *routingIndex) addPattern(pat *pattern) { |
在 routingIndex.addPattern 方法中,逻辑比较简单,直接将 pattern 中的 segment 存储到了当前 index segments 集合中的对应位置。
启动server
1 | func ListenAndServe(addr string, handler Handler) error { |
调用 net/http 包下的公开方法 ListenAndServe,可以实现对服务端的一键启动。内部会声明一个新的 Server 对象,并执行 Server.ListenAndServe 方法。
1 | func (srv *Server) ListenAndServe() error { |
Server.ListenAndServe 方法中,根据用户传入的端口,申请到一个监听器 listener,继而调用 Server.Serve 方法。
1 | var ServerContextKey = &contextKey{"http-server"} |
Server.Serve 方法很核心,体现了 http 服务端的运行架构:for + listener.accept 模式。
- 将 server 封装成一组 kv 对,添加到 context 当中
- 开启 for 循环,每轮循环调用 Listener.Accept 方法阻塞等待新连接到达
- 每有一个新连接到达,就创建一个 goroutine 异步执行 conn.serve 方法负责处理
1 | func (c *conn) serve(ctx context.Context) { |
在 serverHandler.ServeHTTP 方法中,会对 Handler 做判断,倘若其未声明,则取全局单例 DefaultServeMux 进行路由匹配,呼应了 http.HandleFunc 中的处理细节。
1 | func (sh serverHandler) ServeHTTP(rw ResponseWriter, req *Request) { |
接下来,兜兜转转依次调用 ServeMux.ServeHTTP 和 ServeMux.findHandler 方法,然后在 ServeMux.matchOrRedirect 和 routingNode.match 方法中,以 Request 中的 host 和 path 作为 pattern,在已注册的 routingTree 当中进行匹配,最后将匹配到的 handler 进行 handler.ServeHTTP 方法的调用做请求的处理和响应。
1 | func (mux *ServeMux) ServeHTTP(w ResponseWriter, r *Request) { |
1 | func (mux *ServeMux) findHandler(r *Request) (h Handler, patStr string, _ *pattern, matches []string) { |
1 | func (mux *ServeMux) matchOrRedirect(host, method, path string, u *url.URL) (_ *routingNode, matches []string, redirectTo *url.URL) { |
1 | func (root *routingNode) match(host, method, path string) (*routingNode, []string) { |
客户端
核心数据结构
Client
与 Server 相同,客户端模块也有一个 Client 类,实现对整个模块的封装:
1 | type Client struct { |
RoundTripper
1 | type RoundTripper interface { |
RoundTripper 是执行 HTTP 通信的 interface,需要实现方法 RoundTrip,即通过传入请求 Request,与服务端交互后获得响应 Response。
Transport
1 | type Transport struct { |
Transport 是 RoundTripper 的实现类
Request
1 | type Request struct { |
Response
1 | type Response struct { |
方法链路总览
客户端发起一次 http 请求大致分为几个步骤:
- 构造 http 请求参数
- 获取用于与服务端交互的 tcp 连接
- 通过 tcp 连接发送请求
- 通过 tcp 连接接收响应结果
http.Post
调用 net/http 包下的公开方法 Post 时,需要传入服务端地址 url,请求参数格式 contentType 以及请求参数的 io reader。
方法中会使用包下的单例客户端 DefaultClient 来处理这个请求。
1 | var DefaultClient = &Client{} |
Client.Post
在 Client.Post 方法中,首先会根据用户的入参,构造出完整的请求参数 Request;然后通过 Client.Do 方法来处理这个请求。
1 | func (c *Client) Post(url, contentType string, body io.Reader) (resp *Response, err error) { |
NewRequest
在 NewRequest 方法中,直接调用了 NewRequestWithContext 方法。
1 | func NewRequest(method, url string, body io.Reader) (*Request, error) { |
在 NewRequestWithContext 方法中,根据用户传入的 url、method 等信息,构造了 Request 实例。
1 | func NewRequestWithContext(ctx context.Context, method, url string, body io.Reader) (*Request, error) { |
Client.Do
发送请求时,经由 Client.Do -> Client.do 辗转,继而进入到 Client.send 方法中。
1 | func (c *Client) Do(req *Request) (*Response, error) { |
1 | func (c *Client) do(req *Request) (retres *Response, reterr error) { |
在 Client.send 方法中,会在通过 send 方法发送请求之前和之后,分别对 cookie 进行更新。
1 | func (c *Client) send(req *Request, deadline time.Time) (resp *Response, didTimeout func() bool, err error) { |
在调用 send 方法时,需要传入 RoundTripper 实例,这里调用了 Client.transport 方法获取到这一实例:
1 | var DefaultTransport RoundTripper = &Transport{ |
默认会使用全局单例 DefaultTransport。
在 send 方法内部,调用了 Transport.RoundTrip 方法处理核心的请求逻辑:
1 | func send(ireq *Request, rt RoundTripper, deadline time.Time) (resp *Response, didTimeout func() bool, err error) { |
Transport.getConn
1 | func (t *Transport) getConn(treq *transportRequest, cm connectMethod) (_ *persistConn, err error) { |
获取 tcp 连接的策略总体来看分为两步:
- 通过 Transport.queueForIdleConn 方法,尝试复用采用相同协议、访问相同服务端的空闲连接;
- 倘若无空闲连接,则通过 Transport.queueForDial 方法,异步创建一个新的连接,并通过接收 result channel 信号的方式,确认构造连接的工作已经完成。
复用连接
1 | func (t *Transport) queueForIdleConn(w *wantConn) (delivered bool) { |
1 | func (w *wantConn) tryDeliver(pc *persistConn, err error, idleAt time.Time) bool { |
在复用连接这一步,主要包含了以下几个步骤:
- 尝试从连接池(Transport.idleConn 集合)中获取指向同一服务端到空闲连接 persisConn;
- 获取到连接后调用 wantConn.tryDeliver 方法将其发送到 wantConn.result 这个 channel 中;
- 发送成功后,将 wantConn.result channel 关闭。
创建连接
1 | func (t *Transport) queueForDial(w *wantConn) { |
Transport.queueForDial 会调用 Transport.startDialConnForLocked 方法执行创建连接的动作。
1 | func (t *Transport) startDialConnForLocked(w *wantConn) { |
在 Transport.startDialConnForLocked 方法会异步调用 Transport.dialConnFor 方法,创建新的 tcp 连接。
这里之所以采用异步操作创建连接,有两部分原因:
- 一个 tcp 连接并不是一个静态的数据结构,它是有生命周期的,创建过程中会为其创建负责读写的两个守护协程,伴随而生
- 在上游的 Transport.getConn 方法中,当通过 select 多路复用的方式,接收到其他终止信号时,可以提前调用 wantConn.cancel 方法打断创建连接的 goroutine。相比于串行化执行而言,这种异步交互的模式,具有更高的灵活度
1 | func (t *Transport) dialConnFor(w *wantConn) { |
Transport.dialConnFor 方法中,首先调用 Transport.dialConn 方法创建 tcp 连接 persistConn,接着执行 wantConn.tryDeliver 方法,将连接写入 result channel 并唤醒上游进行读取。
1 | func (t *Transport) dialConn(ctx context.Context, cm connectMethod) (pconn *persistConn, err error) { |
Transport.dialConn 方法中包含了创建连接的核心逻辑:
- 调用 Transport.dial 方法,最终通过 Transport.DialContext 和 Transport.Dial 方法创建 tcp 连接
- 异步启动连接的伴生读写协程 readLoop 和 writeLoop 方法,组成提交请求、接收响应的循环
1 | func (t *Transport) dial(ctx context.Context, network, addr string) (net.Conn, error) { |
在伴生读协程 persistConn.readLoop 方法中,会读取来自服务端的响应,并添加到 persistConn.reqCh.ch 中,供上游的 persistConn.roundTrip 方法接收。
1 | func (pc *persistConn) readLoop() { |
1 | func (pc *persistConn) roundTrip(req *transportRequest) (resp *Response, err error) { |
在伴生写协程 persistConn.writeLoop 方法中,会通过 persistConn.writech 方法读取到客户端提交的请求,然后将其发送到服务器。
1 | func (pc *persistConn) writeLoop() { |
1 | func (pc *persistConn) roundTrip(req *transportRequest) (resp *Response, err error) { |
暂存连接
有连接复用的能力,就必然存在存储连接的机制。
首先,在构造新连接中途,倘若被打断,则可能会将连接放回队列以供复用:
1 | func (t *Transport) getConn(treq *transportRequest, cm connectMethod) (pc *persistConn, err error) { |
1 | func (w *wantConn) cancel(t *Transport, err error) { |
1 | func (t *Transport) putOrCloseIdleConn(pconn *persistConn) { |
1 | func (t *Transport) tryPutIdleConn(pconn *persistConn) error { |
其次,倘若与服务端的一轮交互流程结束,也会将连接放回队列以供复用:
1 | func (pc *persistConn) readLoop() { |
persistConn.roundTrip
在上一个小节中提到:一个连接 persistConn 是一个具有生命特征的角色。它本身伴有 readLoop 和 writeLoop 两个守护协程,与上游应用者之间通过 channel 进行读写交互。
而其中扮演应用者这一角色的,正是本小节谈到的主流程中的方法:persistConn.roundTrip。
- 它首先将 http 请求通过 persistConn.writech 发送给连接的守护协程 writeLoop,并进一步传送到服务端
- 其次通过读取 persistConn.reqch.ch channel,接收由守护协程 readLoop 代理转发的客户端响应数据
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21func (pc *persistConn) roundTrip(req *transportRequest) (resp *Response, err error) {
// ...
pc.writech <- writeRequest{req, writeErrCh, continueCh}
resc := make(chan responseAndError)
pc.reqch <- requestAndChan{
req: req.Request,
cancelKey: req.cancelKey,
ch: resc,
// ...
}
// ...
for {
select {
// ...
case re := <resc:
// ...
return re.res, nil
// ...
}
}
}