golang源码分析-启动一个http服务到底发生了什么?

前言

本文将从golang的源码方面分析:如何启动http服务,这个服务是怎么对外提供服务的,以及为什么可以保证高性能。

这里先附上平时看源码的一个小技巧:

  1. 先看库对外暴露的方法(了解该库提供了哪些功能)
  2. 再看库有哪些核心的结构(该库提供的功能肯定依赖于某种数据结构)
  3. 最后看这些核心结构有哪些私有方法(核心的数据结构它是如何实现功能的)

本文代码基于golang 1.18


http协议介绍

这里只是简单介绍下该协议是啥,更详细的可以参考互联网。

Hypertext Transfer Protocol(超文本传输协议)。我对这个名词的理解是这个东西本质是字符串,但是它又不只是字符串而有更加深刻的含义。所以叫超文本[doge]

大白话就是,它是一个应用层的通信协议。现在两台计算机要互相通信,肯定需要遵守某种规范,http就是这种规范。

asd

它基于传输层的tcp协议。大致的请求步骤如下:

  1. 客户端通过http请求向服务器发起通信
  2. 服务器接收到请求后,处理请求
  3. 将处理的结果封装并响应给客户端

基本上就是一问一答的模式,至于http2之后支持的双向通信,这不再本文讨论的范围


golang内启动http服务

第一种方式:

 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.Handlehttp.HandleFunc是将路径和处理器注册到了DefaultServeMux上。

接下来继续往下看,DefaultServeMux.HandleFuncDefaultServeMux.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
	}
}

逻辑都在上面的代码块里注释了,大致总结:

  1. 使用mutex锁进行并发控制
  2. 各种校验(判断请求参数是否正确...)
  3. 保存映射关系至ServeMux.m
  4. 检查路径看是否需要排序和设置hosts值

路径和处理器的映射关系现在我们知道是怎么实现的了,核心就是保存在ServeMux.m中。只不过咱们调用http.Handlehttp.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服务如何启动、请求进来后如何找到对应的处理器并处理请求,都理清楚了。大致总结下:

  1. 通过http.Handlehttp.HandleFunc函数注册请求路径与处理器的映射关系,保存在ServeMux.m字段内
  2. 调用http.ListenAndServe函数启动服务,传入监听的tcp地址和一个http.Handler实例,如果为nil,则使用标准库的http.DefaultServeMux
  3. 函数内部会启用一个for循环,监听来自客户端的socket连接(accept)。如果检测到请求则开启协程去处理
  4. 协程内部调用http.DefaultServeMuxServeHTTP去处理
  5. http.DefaultServeMux.ServeHTTP()实现是:根据请求路径找到最合适的处理器去处理请求(比如例子中根据/ping路径找到了PingHandler{})

我们注意到http.ListenAndServe会要求传入http.Handler,传nil默认使用标准库的DefaultServeMux来进行请求路径的分发处理。咱们可以基于这个来定至自己的ServeMux,自己给请求分发器增加功能(如路由分组等等)。事实上mux仓库也是这么做的。

本文总结完毕,如有不太明白的可以直接在评论区交流~

updatedupdated2023-05-222023-05-22