官方的 http 包

  1. 我们从流量打入端口开始,gin 其实是官方 http 包的封装
1
2
3
4
5
6
7
8
9
// 这里是 gin.Run 的逻辑
func (engine *Engine) Run(addr ...string) (err error) {
	defer func() { debugPrintError(err) }()

	address := resolveAddress(addr)
	debugPrint("Listening and serving HTTP on %s\n", address)
	err = http.ListenAndServe(address, engine)
	return
}
  1. 再回顾下官方 http 包的使用方法
 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
func HelloServer(w http.ResponseWriter, req *http.Request) {
	io.WriteString(w, "hello, world!\n")
}

func run() {
    // 点进去就发现,它使用 DefaultServeMux 注册了路由地址
	http.HandleFunc("/hello", HelloServer)
    // 这里直接可以通过函数名知道功能
    // 其中两个主要功能就是
    // 1. 初始化一个 net.Listener 对象,并 Accept 监听
    // 2. 当有新的 socket 链接时,包装一下这个 socket 链接,再启动一个 goroutine 对 connection 进行处理
    // 2.1 对 connection 的处理,主要是解析发来的报文,并封装成 request、response 对象,调用 DefaultServeMux 进行处理
	http.ListenAndServe("", nil)
}
  1. DefaultServeMux 是什么,做了什么

    DefaultServeMux 是一个 ServeMux 结构体对象,而 ServeMux 实现了 ServeHTTP(w ResponseWriter, r *Request) 方法

    只要实现了 ServeHTTP 方法,就可以接收到 Response、Request 对象并对它们进行处理

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
20
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
}

// 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)
}

gin 作了哪些事情

很明显,gin 实现了一个 Handler 接口(即实现了一个 ServeHTTP(w ResponseWriter, r *Request))方法

下面是代码:它先从 pool 中拿一个自己封装的 Context 对象,再进行一些初始化工作,然后调用 handleHTTPRequest 进行处理,最后将 Context 放回池中

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
// ServeHTTP conforms to the http.Handler interface.
func (engine *Engine) ServeHTTP(w http.ResponseWriter, req *http.Request) {
	c := engine.pool.Get().(*Context)
	c.writermem.reset(w)
	c.Request = req
	c.reset()

    // 匹配路由,调用 handler 函数链进行处理
	engine.handleHTTPRequest(c)

	engine.pool.Put(c)
}

如何benchmark

gin 在路由上比官方 http 更快,我们来探究下原因

  1. gin 采取 []node 形式, node 是个树形结构,数组中元素是不同方法(Get、PUT、POST…)的根节点

  2. http 支持动态路由,使用了 RWMutex;使用了字典的数据结构存储 map[路由]handler

在实际生产中,还是使用 benchmark 压测更有说服力


一些细节

  1. 每个 src/net/http/server.go/Server 结构体存储了每个链接 以及它们的状态(StateNew/StateActive/StateIdle/StateHijacked/StateClosed), 所以可以调用 Server.Shutdown 安全关闭连接,不会关闭正在读写的链接,当然这有超时时间

一些过程中的问题

  1. hijacked

    • 上文提到,官方包将 socket 链接进行了一定的包装,在这个 connection 对象里,存储了状态(字段 curState),状态包含有 StateHijacked
    • 跟随代码,发现可以在 HandleFunc 中使用 Hijack 方法,接管 conn 的读写解析权,这样就可以定制自己的协议了
    • hijack示例
  2. http Header 中的 Expect

    • 当 POST 请求中 body 过大时,客户端需要携带 Expect:100-continue Header,服务端会回复一个 100 Continue 给客户端
    • 服务端发现如果是 Expect 请求,就会将 body 封装成 expectContinueReader 对象,当读取 req.Body 时,就会自动回复客户端 100 Continue
  3. src/net/http/server.go 下的 bufioReaderPool (sync.Pool) 对象为什么没有声明 New

    • 源码采用的是自己控制判断 Pool.Get 到的对象如果是 nil 的话,再实例化化一个 Reader 对象
    • 这样做的好处是,自己控制如果 Get 到 nil 的话,就省去 Reader.reset 的逻辑;否则每次 Get 到对象,都得调用 reset 逻辑

总结

gin 主要工作是替换了官方 http 包的处理引擎,实现了 ServerHttp 的方法。

当然在此基础上,作了很多实用且高效的封装,比如 Middlerware 的引入,context 的封装,参数的解析…

尽管如此,它并没有脱离官方 net/http 的模型,当一个链接打进来时,需要至少一个 goroutine 去处理(为什么是至少一个呢,http1.1 的实现中就是1个;http2 需要两个)

那能否再进一步呢?

使用 epoll 多路复用模型,当一个链接打进来时,我们仅仅将它注册到事件循环中,当链接可读可写时,再从 goroutine 池中拿取一个 g 进行处理

这样避免了空闲 socket 链接占用大量资源