gin 源码探究
官方的 http 包
- 我们从流量打入端口开始,gin 其实是官方 http 包的封装
|
|
- 再回顾下官方 http 包的使用方法
|
|
-
DefaultServeMux 是什么,做了什么
DefaultServeMux 是一个 ServeMux 结构体对象,而 ServeMux 实现了 ServeHTTP(w ResponseWriter, r *Request) 方法
只要实现了 ServeHTTP 方法,就可以接收到 Response、Request 对象并对它们进行处理
|
|
gin 作了哪些事情
很明显,gin 实现了一个 Handler 接口(即实现了一个 ServeHTTP(w ResponseWriter, r *Request))方法
下面是代码:它先从 pool 中拿一个自己封装的 Context 对象,再进行一些初始化工作,然后调用 handleHTTPRequest 进行处理,最后将 Context 放回池中
|
|
gin 在路由上比官方 http 更快,我们来探究下原因
-
gin 采取 []node 形式, node 是个树形结构,数组中元素是不同方法(Get、PUT、POST…)的根节点
-
http 支持动态路由,使用了 RWMutex;使用了字典的数据结构存储 map[路由]handler
在实际生产中,还是使用 benchmark 压测更有说服力
一些细节
- 每个 src/net/http/server.go/Server 结构体存储了每个链接 以及它们的状态(StateNew/StateActive/StateIdle/StateHijacked/StateClosed), 所以可以调用 Server.Shutdown 安全关闭连接,不会关闭正在读写的链接,当然这有超时时间
一些过程中的问题
-
hijacked
- 上文提到,官方包将 socket 链接进行了一定的包装,在这个 connection 对象里,存储了状态(字段 curState),状态包含有 StateHijacked
- 跟随代码,发现可以在 HandleFunc 中使用 Hijack 方法,接管 conn 的读写解析权,这样就可以定制自己的协议了
- hijack示例
-
http Header 中的 Expect
- 当 POST 请求中 body 过大时,客户端需要携带
Expect:100-continue
Header,服务端会回复一个100 Continue
给客户端 - 服务端发现如果是 Expect 请求,就会将 body 封装成 expectContinueReader 对象,当读取 req.Body 时,就会自动回复客户端
100 Continue
- 当 POST 请求中 body 过大时,客户端需要携带
-
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 链接占用大量资源