HTTP 源码解析

Go 语言以其出色的并发性能和优雅的编程模型而闻名,对于 http 服务可以做到开箱即用,无需第三方框架,而且使用起来也很简单。即便如此,还会有很多 http 框架的诞生,例如 ginecho 等,说明自带的 http 服务还有不完美的地方,导致了用户选择第三方开发的框架。

这是一个采用标准库开发的 http 服务。

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
package main

import "net/http"

func main() {
	http.HandleFunc("/ping", pingHandler)
	http.ListenAndServe(":8000", nil)
}

func pingHandler(w http.ResponseWriter, r *http.Request) {
	w.Write([]byte("pong"))
}

这是一个简单的HTTP服务。在启动服务后,通过访问 http://localhost:8000/ping,可以访问相关的处理程序并返回响应信息:“pong”。

这段简单的代码中,直接运行了一个高性能的HTTP服务。代码中涉及了两个与HTTP相关的函数,分别是:http.HandleFunchttp.ListenAndServe

接下来,对这两个函数进行解析,让您了解每个步骤的具体操作。

HandleFunc函数的作用是将指定的处理函数handle注册到HTTP框架中,使得特定的URL路径和该处理函数建立映射关系。

以下是HandleFunc()函数的源代码实现

1
2
3
func HandleFunc(pattern string, handler func(ResponseWriter, *Request)) {
	DefaultServeMux.HandleFunc(pattern, handler)
}

在这段代码中,HandleFunc函数接收两个参数:patternhandlepattern是URL路径的模式,用于匹配请求的URL;handle是处理函数,接收ResponseWriterRequest作为参数,用于处理请求并生成相应的响应。

函数内部调用了DefaultServeMux.HandleFunc()函数,将patternhandle注册到默认的 ServeMux(多路复用器)中,建立URL和处理函数之间的映射关系。

1
2
3
4
5
6
func (mux *ServeMux) HandleFunc(pattern string, handler func(ResponseWriter, *Request)) {
	if handler == nil {
		panic("http: nil handler")
	}
	mux.Handle(pattern, HandlerFunc(handler))
}

HandleFunc函数是ServeMux类型的方法。ServeMux是一个HTTP请求多路复用器,用于将请求路由到相应的处理程序。在这个方法中,我们传入了一个pattern参数和一个handler函数参数。

1
2
3
4
5
6
type HandlerFunc func(ResponseWriter, *Request)

// ServeHTTP calls f(w, r).
func (f HandlerFunc) ServeHTTP(w ResponseWriter, r *Request) {
	f(w, r)
}

HandlerFunc 的定义很简单,并且实现了 ServeHTTP 方法。这个方法主要是调用本身。

mux.Handler实现同样比较简单,可以看到将路由(pattern)和具体实现(handler)注册到 DefaultServeMux 这个对象上。通过不断的往下看源码,可以找到 ServeMux.Handler 这个方法上。这个方法主要是将服务中的路由进行注册。将服务注册到 ServerMux 对象上,也就是上文所提到的 DefaultServeMux

对于 ServeMux 来说,是一个比较简单的结构。其中可以看到 mes 保存了路由

 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
var DefaultServeMux = &defaultServeMux
var defaultServeMux ServeMux

type ServeMux struct {
	mu    sync.RWMutex
	m     map[string]muxEntry
	es    []muxEntry
	hosts bool
}

type muxEntry struct {
	h       Handler
	pattern string
}

func (mux *ServeMux) Handle(pattern string, handler Handler) {
	// 省略代码 ...
	if mux.m == nil {
		mux.m = make(map[string]muxEntry)
	}
	e := muxEntry{h: handler, pattern: pattern}
	mux.m[pattern] = e
	if pattern[len(pattern)-1] == '/' {
		mux.es = appendSorted(mux.es, e)
	}

	if pattern[0] != '/' {
		mux.hosts = true
	}
}

如果 ServerMux.m 为空,会进行初始化,之后将 handlerpattern 存放到 muxEntry 中,最后将 muxEntry 存放到 m 中,m 的 key 是 pattern。这里的 pattern 就是 url 路径。

在 ServeMux 中,mux.es 切片是用来保存以斜杠结尾的路由模式对应的 muxEntry 对象的。它的作用是在请求的 URL 中去掉末尾的斜杠后进行匹配,从而避免重复处理类似 /path 和 /path/ 这样的 URL。

例如,如果有两个路由模式分别为 /path 和 /path/,请求的 URL 为 /path/,如果没有 mux.es 切片,将会尝试匹配 /path 和 /path/ 两个路由模式,最终会选择匹配 /path/ 的路由模式进行处理。这会导致处理器被重复调用。而使用 mux.es 切片,请求的 URL 会被处理为 /path,只会匹配到 /path 这一个路由模式,避免了处理器被重复调用的问题。

因此,mux.es 切片的作用是为了提高 ServeMux 的匹配效率,避免重复处理请求。

所有的 url 和 Handler 的映射关系都是通过 map[string]muxEntry 进行保存。这样就会出现问题,稍微复杂一些的 url 就无法很好的匹配。这也就是为什么会有大量的 go web 框架,而这些框架都是改写路由的匹配算法。

关于 DefaultServeMux 是一个实现了 HandlerServe 接口的结构体。

上面就是一个主要的注册过程。

这个方法主要对端口进行监听。

1
2
3
4
func ListenAndServe(addr string, handler Handler) error {
	server := &Server{Addr: addr, Handler: handler}
	return server.ListenAndServe()
}

从源码可以看到,这里需要一个 handler 参数,并且新生成一个 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)
}

在 ListenAndServe 中,首先对服务的状态进行了判断,如果是 shuttingDown 就提示 http: Server closed。这里的 shuttingDown 主要是通过一个叫 atomicBool 进行判断的。咋一看以为是原子操作,仔细看其实是定义了一个 int32 类型,通过 int32 的原子操作保证了并发安全。

1
2
3
4
5
type atomicBool int32

func (b *atomicBool) isSet() bool { return atomic.LoadInt32((*int32)(b)) != 0 }
func (b *atomicBool) setTrue()    { atomic.StoreInt32((*int32)(b), 1) }
func (b *atomicBool) setFalse()   { atomic.StoreInt32((*int32)(b), 0) }

之后通过 net.Listen() 方法进行监听,这里对这个方法不做过多的赘述,之后通过 Serve 方法。 下面是主要的核心方法

 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
ctx := context.WithValue(baseCtx, ServerContextKey, srv)
for {
	rw, err := l.Accept()
	if err != nil {
		select {
		case <-srv.getDoneChan():
			return ErrServerClosed
		default:
		}
		if ne, ok := err.(net.Error); ok && ne.Temporary() {
			if tempDelay == 0 {
				tempDelay = 5 * time.Millisecond
			} else {
				tempDelay *= 2
			}
			if max := 1 * time.Second; tempDelay > max {
				tempDelay = max
			}
			srv.logf("http: Accept error: %v; retrying in %v", err, tempDelay)
			time.Sleep(tempDelay)
			continue
		}
		return err
	}
	connCtx := ctx
	if cc := srv.ConnContext; cc != nil {
		connCtx = cc(connCtx, rw)
		if connCtx == nil {
			panic("ConnContext returned nil")
		}
	}
	tempDelay = 0
	c := srv.newConn(rw)
	c.setState(c.rwc, StateNew, runHooks) // before Serve can return
	go c.serve(connCtx)
}

l 为 net.Listener 对象,当每次接收到信息的时候,首先会进行一个错误判断。 如果是 down 的信号,就会直接返回相关错误,否则先对错误进行断言,检查是否为 net.Error,这个是一个接口,其中 Temporary 方法官方已经标记为启用,这个方法更多的表示为超时。如果有超时,你们就会对延时 tempDelay,进行增加,起初是 5 毫秒,之后每次增加 2 倍,最大为 1 秒钟,之后会进行重试。

通过 srv.ConnContext 会生成一个新的 ctx,否则就使用之前的 ctx,也就是 context.Backgroud()。然后开启一个协程进行服务。

在协程中,通过 readRequest 方法进行获取,返回 response。通过 response 对象获取 request。通过 request 判断请求是否要继续。

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
req := w.req
if req.expectsContinue() {
	if req.ProtoAtLeast(1, 1) && req.ContentLength != 0 {
		req.Body = &expectContinueReader{readCloser: req.Body, resp: w}
		w.canWriteContinue.Store(true)
	}
} else if req.Header.get("Expect") != "" {
	w.sendExpectationFailed()
	return
}

这里判断首先通过 expectsContinue 方法,这个方法中获取请求头中的 Expect 字段是否等于 100-continue。当等于的时候要继续进行判断,其中请求的协议为 HTTP 1.1 和 ContentLength 不为 0。这样就可以获取到请求体。

当请求头中的 Expect 和上述条件不相同的时候,直接返回 417 错误。

之后创建了一个 serverHandler 并且调用了 ServeHTTP 并且传入了 response 和 request。

ServeHTTP 再一次出现,其中第一步就是获取 Handler。

1
2
3
4
handler := sh.srv.Handler
if handler == nil {
	handler = DefaultServeMux
}

那么,这里获取的 handler 应该是什么呢?经过多个方法或函数,可以已经对 sh.srv.Handler 一步一步的向上推到。这里我将这个过程画了一张图,图上箭头表示关系之间的依赖,红色表示持有 handler 数据。

https://island-hexo.oss-cn-beijing.aliyuncs.com/http_handler.png

通过这个依赖图可以看到,handler 是由最开始的 ListenAndServe 方法进行传入的,而我们的示例代码中这部分传入的是 nil,也就是从开始到现在 handler 一直为 nil。这也就是为什么会一个判断,当 handler 为空的时候使用 DefaultServeMux。其实关于默认的 handler 为 DefaultServeMux 这个事情,在 ListenAndServe 这个代码的注释中就已经说明。

之后就是调用 handler.ServeHTTP 方法,ServeHTTP 方法主要是一个请求分发的作用。源码中调用了 Handler 方法。这个方法主要的作用就是查找请求对应的 Handler 是那个。

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
// ServeHTTP dispatches the request to the handler whose
// pattern most closely matches the request URL.
func (mux *ServeMux) ServeHTTP(w ResponseWriter, r *Request) {
	if r.RequestURI == "*" {
		if r.ProtoAtLeast(1, 1) {
			w.Header().Set("Connection", "close")
		}
		w.WriteHeader(StatusBadRequest)
		return
	}
	h, _ := mux.Handler(r)
	h.ServeHTTP(w, r)
}

先对方法为 CONNECT 的请求做了处理。通过 redirecToPathSlash 方法,这个方法主要是要判断给定的路径是否要加 /,而这个方法中加锁后调用了 shouldRedirectRLocked 这个方法。通过查找 ServeMux 的 m 这个属性中是否存在相关的路径。这个 m 在上文中介绍过,是一个 map 结构。这也就是为什么会加锁,这里的 map 是并发不安全的。查询的方式也很简单,就是在之前的 map 中查询是否存在,如果存在就返回 false,如果没有找到会进行一些特殊的处理,在路径上加上后缀 / 进行查找,同时为了防止路径为 //,所以在返回的时候又进行了一次判断。

在上述的方法结束后,会返回一个 bool 值,来确定是否需要添加末尾的 /。从而返回相关的 url。通过新的 url 生成一个 RedirectHandler 的结构体。如果没有找到通过 通过 handler 这个方法。

handler 这个方法通过 match 这个方法进行查找。

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
func (mux *ServeMux) match(path string) (h Handler, pattern string) {
	v, ok := mux.m[path]
	if ok {
		return v.h, v.pattern
	}

	for _, e := range mux.es {
		if strings.HasPrefix(path, e.pattern) {
			return e.h, e.pattern
		}
	}
	return nil, ""
}

这个方法比较简单,现在 m 中进行查找,如果没有找到,从 es 中查找。之前我们说过 es 是保存了后缀有 / 的 handler。如果都无法查找到就返回 nil。

这里的 handler 就是在一开始注册的 HandleFunc(pingHandler)

最后在 ServeMux.ServeHTTP 这个方法中调用 handler 相关的 ServeHTTP。这样就调用成功了。

通过两个方法基本可以做到路由的注册方式和路由的查询方式,并且对请求来临的时候相关的处理过程。这些方法对之后研究其他框架源码或者工作方式更加清晰。

相关内容