本文将从golang的源码方面分析:如何启动http服务,这个服务是怎么对外提供服务的,以及为什么可以保证高性能。
这里先附上平时看源码的一个小技巧:
- 先看库对外暴露的方法(了解该库提供了哪些功能)
- 再看库有哪些核心的结构(该库提供的功能肯定依赖于某种数据结构)
- 最后看这些核心结构有哪些私有方法(核心的数据结构它是如何实现功能的)
本文代码基于golang 1.18
这里只是简单介绍下该协议是啥,更详细的可以参考互联网。
Hypertext Transfer Protocol(超文本传输协议)。我对这个名词的理解是这个东西本质是字符串,但是它又不只是字符串而有更加深刻的含义。所以叫超文本[doge]
大白话就是,它是一个应用层的通信协议。现在两台计算机要互相通信,肯定需要遵守某种规范,http就是这种规范。
它基于传输层的tcp协议。大致的请求步骤如下:
- 客户端通过http请求向服务器发起通信
- 服务器接收到请求后,处理请求
- 将处理的结果封装并响应给客户端
基本上就是一问一答的模式,至于http2
之后支持的双向通信,这不再本文讨论的范围
第一种方式:
1
2
3
4
5
6
7
8
9
10
|
type PingHandler struct {}
func (h *PingHandler) ServeHTTP(w http.ResponseWriter, r *http.Request) {
fmt.Fprint(w, "pong")
}
func main() {
http.Handle("/ping", &PingHandler{})
log.Fatal(http.ListenAndServe(":8080", nil))
}
|
第二种方式:
1
2
3
4
5
6
|
func main() {
http.HandleFunc("/ping", func(w http.ResponseWriter, r *http.Request) {
fmt.Fprint(w, "pong")
})
log.Fatal(http.ListenAndServe(":8080", nil))
}
|
两种方式启动http都特别简单,只需要几行代码即可。接下来我们一一看。
1
2
3
|
func Handle(pattern string, handler Handler)
Handle registers the handler for the given pattern in the DefaultServeMux.
The documentation for ServeMux explains how patterns are matched.
|
方式一使用到了Handle
函数,这个函数定义如上,本质上是为了注册路径和处理器之间的映射。在方式1的例子中,我们向localhost:8080/ping
路径注册了PingHandler
,这个PingHandler
实现了http.Handler
接口,也就是Handle
函数的第二个参数。
http.Handler
接口定义:
1
2
3
4
|
type Handler interface {
ServeHTTP(ResponseWriter, *Request)
}
A Handler responds to an HTTP request.
|
这个接口只有一个方法,叫ServeHTTP
。意为处理HTTP请求,参数一是响应封装,参数二是HTTP请求封装。例子中的PingHandler
实现了该接口,当请求进来时,我们会回复一个pong
字符串给客户端。
注册完路径和处理器之间的映射后,就需要启动服务了,启动服务需要调用该函数:
1
2
3
4
|
func ListenAndServe(addr string, handler Handler) error
ListenAndServe listens on the TCP network address addr and then calls Serve
with handler to handle requests on incoming connections. Accepted
connections are configured to enable TCP keep-alives.
|
第一个参数是要监听的tcp网络地址,我们这里监听的是本机的8080端口,而localhost
可以省略不写,所以就填入:8080
。第二个参数下文再说,这里先填入nil
到此为止,http服务就启动成功了,你们可以复制代码测试下。接下来看方式二。方式二区别在于注册映射的时候,官方提供了一个语法糖。
1
2
3
4
|
func HandleFunc(pattern string, handler func(ResponseWriter, *Request))
HandleFunc registers the handler function for the given pattern in the
DefaultServeMux. The documentation for ServeMux explains how patterns are
matched.
|
这个函数和Handle
函数类似,第一个参数还是请求路径,第二个参数则变成了一个处理函数。这个处理函数的签名和http.Handler.ServeHTTP
一模一样,也就意味着我们不需要实现http.Handler
接口,直接传入一个处理函数就可以注册了。
具体的可以参照上面的方式二,在实际中也推荐方式二。这样就不用每个路径都实现一次http.Handler
接口。
上文提到的两种注册路径与映射的函数,他们实现:
1
2
3
4
5
|
func Handle(pattern string, handler Handler) { DefaultServeMux.Handle(pattern, handler) }
func HandleFunc(pattern string, handler func(ResponseWriter, *Request)) {
DefaultServeMux.HandleFunc(pattern, handler)
}
|
这两个函数都依赖了DefaultServeMux
,它是ServeMux
的实例,结构如下:
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
|
type ServeMux struct {
// 读写锁,保证并发安全
mu sync.RWMutex
// 保存路径与处理器之间的映射
m map[string]muxEntry
// 根据路径长度从高到低给处理器排序
es []muxEntry // slice of entries sorted from longest to shortest.
// 是否有一个路径包含hostname
hosts bool // whether any patterns contain hostnames
}
type muxEntry struct {
// 处理器
h Handler
// 路径
pattern string
}
|
字段我注释在代码块上了,看字段也可以猜出,这个结构体就是保存路径和处理器的映射关系的。而DefaultServeMux
是该结构体的一个默认值,它是一个全局变量,定义如下:
1
2
3
4
|
// DefaultServeMux is the default ServeMux used by Serve.
var DefaultServeMux = &defaultServeMux
var defaultServeMux ServeMux
|
这样设计的好处是使用者不需要了解具体的数据结构,直接调用函数即可完成注册。到这一步我们知道了调用http.Handle
和http.HandleFunc
是将路径和处理器注册到了DefaultServeMux
上。
接下来继续往下看,DefaultServeMux.HandleFunc
和DefaultServeMux.Handle
:
1
2
3
4
5
6
7
|
// HandleFunc registers the handler function for the given pattern.
func (mux *ServeMux) HandleFunc(pattern string, handler func(ResponseWriter, *Request)) {
if handler == nil {
panic("http: nil handler")
}
mux.Handle(pattern, HandlerFunc(handler))
}
|
可以看到ServeMux.HandleFunc
只是判断了一下handler是否为空,然后就强转为HandlerFunc
,接着调用ServeMux.Handle
了。HandlerFunc
就是上文说到的语法糖,那个和http.Handler.ServeHTTP
一模一样的函数签名。
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
|
// Handle registers the handler for the given pattern.
// If a handler already exists for pattern, Handle panics.
func (mux *ServeMux) Handle(pattern string, handler Handler) {
// 并发控制
mux.mu.Lock()
defer mux.mu.Unlock()
// 判断传入路径是否为空
if pattern == "" {
panic("http: invalid pattern")
}
// 判断传入的处理器是否为空
if handler == nil {
panic("http: nil handler")
}
// 判断对应的路径之前是否注册过,如果注册过则panic
if _, exist := mux.m[pattern]; exist {
panic("http: multiple registrations for " + pattern)
}
// 懒加载ServeMux的映射map
if mux.m == nil {
mux.m = make(map[string]muxEntry)
}
// 给映射map赋值(key: pattern, value: handler)
e := muxEntry{h: handler, pattern: pattern}
mux.m[pattern] = e
if pattern[len(pattern)-1] == '/' {
// 如果路径的最后一位是/结尾的,则给es字段排序
mux.es = appendSorted(mux.es, e)
}
if pattern[0] != '/' {
// 如果路径第一个字符不是/开始的,则设置hosts为true
mux.hosts = true
}
}
|
逻辑都在上面的代码块里注释了,大致总结:
- 使用mutex锁进行并发控制
- 各种校验(判断请求参数是否正确...)
- 保存映射关系至
ServeMux.m
中
- 检查路径看是否需要排序和设置hosts值
路径和处理器的映射关系现在我们知道是怎么实现的了,核心就是保存在ServeMux.m
中。只不过咱们调用http.Handle
和http.HandleFunc
的时候golang帮我们存到了一个全局变量DefaultServeMux
中了。接下来看下是如何启动服务的。
1
2
3
4
5
6
7
8
9
10
11
12
|
// ListenAndServe listens on the TCP network address addr and then calls
// Serve with handler to handle requests on incoming connections.
// Accepted connections are configured to enable TCP keep-alives.
//
// The handler is typically nil, in which case the DefaultServeMux is used.
//
// ListenAndServe always returns a non-nil error.
func ListenAndServe(addr string, handler Handler) error {
server := &Server{Addr: addr, Handler: handler}
// 将addr和handler保存至Server实例内,并调用Server.ListenAndServe
return server.ListenAndServe()
}
|
可以看到ListenAndServe
的实现比较简单,还是要往下看Server.ListenAndServe
1
2
3
4
5
6
7
8
9
10
11
12
13
14
|
func (srv *Server) ListenAndServe() error {
if srv.shuttingDown() {
return ErrServerClosed
}
addr := srv.Addr
if addr == "" {
addr = ":http"
}
ln, err := net.Listen("tcp", addr)
if err != nil {
return err
}
return srv.Serve(ln)
}
|
可以看到这个方法会对addr进行tcp监听,并且调用Server.Serve
方法,接着看
1
2
3
4
5
6
7
8
9
10
11
12
13
14
|
func (srv *Server) Serve(l net.Listener) error {
// ...略
var tempDelay time.Duration // how long to sleep on accept failure
ctx := context.WithValue(baseCtx, ServerContextKey, srv)
for {
rw, err := l.Accept()
// ...略
c := srv.newConn(rw)
c.setState(c.rwc, StateNew, runHooks) // before Serve can return
go c.serve(connCtx)
}
}
|
方法有点长,所以我省略了一部分,直接看重要的逻辑。
for循环内有一个rw, err := l.Accept()
。在socket编程内accept
表示服务器接收一个请求并处理,所以这一行是为了接收来自客户端的请求。
接着c := srv.newConn(rw)
是创建了一个http.conn
的对象,然后通过协程调用http.conn.serve
方法(因为有协程,所以高性能)
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
|
// Serve a new connection.
func (c *conn) serve(ctx context.Context) {
// ...略
for {
// 读取请求参数
w, err := c.readRequest(ctx)
// ...略
c.curReq.Store(w)
// 将conn.server赋给serverHandler并调用serveHTTP进行处理
serverHandler{c.server}.ServeHTTP(w, w.req)
// ...略
// 结束请求
w.finishRequest()
// ...略
c.setState(c.rwc, StateIdle, runHooks)
c.curReq.Store((*response)(nil))
}
}
|
这个方法也是直接看核心逻辑,先读取请求,然后调用http.serverHandler.ServeHTTP
。该方法底层也是调用了http.Handler.ServeHTTP
。
到这里仍然有一个问题,我们最开始http.ListenAndServe(":8080", nil)
的第二个参数传的是nil,那最后nil.ServeHTTP
不是会空指针吗?
看看http.serverHandler.ServeHTTP
里面是怎么做的:
1
2
3
4
5
6
7
8
9
10
11
|
func (sh serverHandler) ServeHTTP(rw ResponseWriter, req *Request) {
handler := sh.srv.Handler
if handler == nil {
// 如果为空, 则赋值为DefaultServeMux
handler = DefaultServeMux
}
// ...略
// 调用ServeHTTP方法
handler.ServeHTTP(rw, req)
}
|
如果为nil,该nil会变为DefaultServeMux。然后再调用DefaultServeMux.ServeHTTP
方法。
1
2
3
4
5
6
7
8
9
|
// ServeHTTP dispatches the request to the handler whose
// pattern most closely matches the request URL.
func (mux *ServeMux) ServeHTTP(w ResponseWriter, r *Request) {
// ...略
// 根据请求找到对应的处理器
h, _ := mux.Handler(r)
// 调用处理器的ServeHTTP方法处理请求
h.ServeHTTP(w, r)
}
|
该方法的逻辑补充到注释上了。至此golang中路径与处理器如何匹配注册、http服务如何启动、请求进来后如何找到对应的处理器并处理请求,都理清楚了。大致总结下:
- 通过
http.Handle
或http.HandleFunc
函数注册请求路径与处理器的映射关系,保存在ServeMux.m字段内
- 调用
http.ListenAndServe
函数启动服务,传入监听的tcp地址和一个http.Handler
实例,如果为nil,则使用标准库的http.DefaultServeMux
- 函数内部会启用一个for循环,监听来自客户端的socket连接(accept)。如果检测到请求则开启协程去处理
- 协程内部调用
http.DefaultServeMux
的ServeHTTP
去处理
- 而
http.DefaultServeMux.ServeHTTP()
实现是:根据请求路径找到最合适的处理器去处理请求(比如例子中根据/ping
路径找到了PingHandler{}
)
我们注意到http.ListenAndServe
会要求传入http.Handler
,传nil默认使用标准库的DefaultServeMux
来进行请求路径的分发处理。咱们可以基于这个来定至自己的ServeMux
,自己给请求分发器增加功能(如路由分组等等)。事实上mux仓库也是这么做的。
本文总结完毕,如有不太明白的可以直接在评论区交流~