BlowFish_Gin源码阅读

持续整理中,不适合阅读

GIN版本 commithash a71af9c144f9579f6dbe945341c1df37aaf09c0d

Gin框架的特点

  • 快:路由使用radix trie实现,检索路径短。无反射。API性能可预测。
  • 支持中间件:请求可以有多个中间件逐个处理,最后交给业务处理。例如:Logger,Authorization,GZIP,最后写入数据库。
  • 若发生了panic,Gin可以捕获并恢复错误,因此服务并不会终止,且可有机会介入错误恢复的过程。
  • JSON校验:Gin可以解析并校验请求的json数据,例如检查字段值。
  • 路由分组:更好的组织路由。通过分组将需要鉴权和不需鉴权的路由分开,分组可以无限嵌套且不影响性能。
  • 错误管理:Gin可以和很方便的收集错误信息。最后使用中间件将错误写入文件或数据库或发送到网络上。
  • 内置视图渲染:提供了易用的接口来渲染JSONXMLHTML
  • 可扩展:自定义中间件非常容易。

源代码阅读

服务启动

Socket Server VS HTTP Server

HTTP是应用层协议;Socket是系统提供的抽象接口,它直接操作传输层协议(如TCPUDP等)来工作。它们不是一个层级上的概念。
所以,只要Socket两端不主动关闭连接,就可以通过TCP连接来双向通信。
而HTTP服务器则按照HTTP协议来通信:建立TCP连接 🡺 客户端发送报文 🡺 服务器相应报文 🡺 客户端或服务器关闭连接。每一个请求都要重复这个过程。虽说TCP协议是长连接的,但上层的HTTP协议会主动关闭它。
另外HTTP中有一个Connection: keep-alive头信息,来重用连接,减少创建连接的消耗。它受到重用次数和超时时间的限制(服务器设置),触发限制时仍会主动断开连接。因此这个所谓的”长连接”和Socket长连接的本质是不同的。

Socket Server例子,内层的for循环读并不会主动关闭连接(不发生panic时)

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
func main() {
srv, err := net.Listen("tcp", ":8080") // 协议,端口
if err != nil {
panic(err)
}
defer srv.Close()

for {
conn, err := srv.Accept() // 监听连接
if err != nil {
fmt.Println("accept failed:", err.Error())
continue
}

go func(c net.Conn){
defer c.Close()
buf := make([]byte, 1024)
for {
n, err := c.Read(buf) // 尝试读数据
if err != nil {
fmt.Println("read failed:", err.Error())
continue
}

receiveData := buf[:n] // 接收到的字节buf[0:n]
fmt.Println("received data=", receiveData)
}
}(conn)
}
}

HTTP Server

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
type handler struct {
}

func (h handler) ServeHTTP(w http.ResponseWriter, r *http.Request) {
requestUrl := r.URL.String()
msg := fmt.Sprintf("request uri=%s\n", requestUrl)
fmt.Println(msg)
_, _ = w.Write([]byte(msg))
}
func main() {
err := http.ListenAndServe("127.0.0.1:8080", handler{}) //地址、端口,处理句柄
if err != nil {
panic(err)
}
}

HTTP Server的底层还是TCP连接,对比上面Socket Server的代码,我们期望在HTTP Server的实现里发现

  1. 创建连接net.Listen
  2. 网络监听srv.Accept()
  3. 读取数据c.Read(buf)
  4. 额外的,在服务端发送完数据后,应该要关闭连接

带着以上四个目标,我们来跟一下HTTP Server的启动过程。

  1. 启动HTTP Servererr := http.ListenAndServe("127.0.0.1:8080", handler{})  
  2. 构造server对象  
1
2
3
4
func ListenAndServe(addr string, handler Handler) error {
server := &Server{Addr: addr, Handler: handler}
return server.ListenAndServe()
}
  1. 调用server的ListenAndServe方法。在#Line9我们发现了net.Listen("tcp", addr)目标1找到
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)
}
  1. 跟入#Line13行代码srv.Serve(ln)  。这里,#Line4:rw,err := l.Accept()目标2找到
    这里的rw即是net.Conn,在#Line14重新包装了rw  ,,并在#Line14启动协程go c.serve(connCtx)
    到此,服务器已经正常启动,并且给每一个新进来的Request都分配了一个协程。#Line3的for循环配合golang轻协程的特性,一个高并发的web服务器启动了。
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
func (srv *Server) Serve(l net.Listener) error {
...
for {
rw, err := l.Accept()
...
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) // before Serve can return
go c.serve(connCtx)
}
}
  1. 继续挖go c.serve(connCtx)看看net/http是如何处理一个Request的。先快速扫一下这个函数里面做了哪些事情:
  2. #Line20w, err := c.readRequest(ctx)构建Response对象。向内追找到HTTP协议的解析过程newTextprotoReader目标3找到
  3. #Line35serverHandler{c.server}.ServeHTTP(w, w.req) 处理业务逻辑(即用户定义的路由逻辑)。ServeHTTP的第一个参数w就是Response对象,负责向客户端响应数据,w.req即Request,负责解析请求参数、头信息等。
  4. #Line40w.finishRequest()中有flush操作,到这里服务器已经完成了数据响应。
  5. #Line50-64处理了keep-alive重用连接和idle_timeout空闲超时断开连接的逻辑。这里涉及到一些网络知识不具体展开。
    若设置了Connection: close或者服务器保持连接直到空闲超时,都会return从而执行#Line5中的defer代码,注意源代码中的#Line1775~1777  目标4找到
  6. 需要额外关注一下#Line35行上面的注释  。这里明确指出了net/http没有实现pipeline,理由是在HTTP1.1中pipeline并没有被(客户端/浏览器)广泛的实现,因此扔到了和HTTP2.0一起实现。
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
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
// Serve a new connection.
func (c *conn) serve(ctx context.Context) {
c.remoteAddr = c.rwc.RemoteAddr().String()
ctx = context.WithValue(ctx, LocalAddrContextKey, c.rwc.LocalAddr())
defer func() {...}()

...

// HTTP/1.x from here on.

ctx, cancelCtx := context.WithCancel(ctx)
c.cancelCtx = cancelCtx
defer cancelCtx()

c.r = &connReader{conn: c}
c.bufr = newBufioReader(c.r)
c.bufw = newBufioWriterSize(checkConnErrorWriter{c}, 4<<10)

for {
w, err := c.readRequest(ctx)
if c.r.remain != c.server.initialReadLimitSize() {
// If we read any bytes off the wire, we're active.
c.setState(c.rwc, StateActive)
}
if err != nil {...}

// Expect 100 Continue support
req := w.req
if req.expectsContinue() {...}

c.curReq.Store(w)

if requestBodyRemains(req.Body) {...}

serverHandler{c.server}.ServeHTTP(w, w.req)
w.cancelCtx()
if c.hijacked() {
return
}
w.finishRequest()
if !w.shouldReuseConnection() {
if w.requestBodyLimitHit || w.closedRequestBodyEarly() {
c.closeWriteAndWait()
}
return
}
c.setState(c.rwc, StateIdle)
c.curReq.Store((*response)(nil))

if !w.conn.server.doKeepAlives() {
// We're in shutdown mode. We might've replied
// to the user without "Connection: close" and
// they might think they can send another
// request, but such is life with HTTP/1.1.
return
}

if d := c.server.idleTimeout(); d != 0 {
c.rwc.SetReadDeadline(time.Now().Add(d))
if _, err := c.bufr.Peek(4); err != nil {
return
}
}
c.rwc.SetReadDeadline(time.Time{})
}
}

P.S. 这里再额外挖一下#Line35serverHandler{c.server}.ServeHTTP(w, w.req)的实现,将用户代码和net/http包打通。
这里首先构造了一个serverHandler对象并调用了它的ServeHTTP方法。 
之后,调用了sh.srv.Handler.ServeHTTP(rw, req),这里的srv就是本文步骤2中构造server对象的这个server对象。
因此这里的.Handler.ServeHTTP最终调用的是我们的HTTP Serverdemo中#Line4-9的代码。

Gin的启动过程

挖完了net/http包,对http网络请求的过程有了一个整体的认知,接下来正式开挖Gin。

  1. 启动服务非常简便engine := gin.New()然后engine.Run(":8080")  
1
2
3
4
5
6
7
8
9
func main() {
engine := gin.New()

//engine.GET("/someGet", getting)
...
//engine.Use(middlewares.Authenticate())

engine.Run(":8080")
}
  1. gin.New()的细节。其中Engine的结构  
    其中:
  • RedirectTrailingSlash若请求地址是/foo/且未匹配,但/foo可以匹配,则将客户端重定向到/foo,若请求是GET则状态码是301,其他动词则是307
  • RedirectFixedPath未匹配时尝试去除多余的..///以修正路径(且转化为小写),例如/FOO/..//FOO都能匹配/foo
  • HandleMethodNotAllowed未匹配时尝试其他动词,若路由匹配则以状态码405响应,否则将请求代理到NotFound句柄。
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
// New returns a new blank Engine instance without any middleware attached.
// By default the configuration is:
// - RedirectTrailingSlash: true
// - RedirectFixedPath: false
// - HandleMethodNotAllowed: false
// - ForwardedByClientIP: true
// - UseRawPath: false
// - UnescapePathValues: true
func New() *Engine {
debugPrintWARNINGNew() //debug模式下打印开发模式警告
engine := &Engine{
RouterGroup: RouterGroup{ // 路由分组
Handlers: nil,
basePath: "/",
root: true,
},
FuncMap: template.FuncMap{}, // 模板函数?
RedirectTrailingSlash: true,
RedirectFixedPath: false,
HandleMethodNotAllowed: false,
ForwardedByClientIP: true,
AppEngine: defaultAppEngine,
UseRawPath: false,
UnescapePathValues: true,
MaxMultipartMemory: defaultMultipartMemory,
trees: make(methodTrees, 0, 9),
delims: render.Delims{Left: "{{", Right: "}}"},
secureJsonPrefix: "while(1);",
}
engine.RouterGroup.engine = engine
engine.pool.New = func() interface{} { // 连接池
return engine.allocateContext()
}
return engine
}
  1. engine.Run(":8080")中的细节。它仅仅是http.ListenAndServe(address, engine)的语法糖,啥也没做。
    因此可以看出来,Gin对网络底层没做任何处理,直接使用了net/http包。其核心代码全部在Engine这个结构体中。根据我们分析net/http包的经验,Engine中一定实现了ServeHTTP方法
1
2
3
4
5
6
7
8
9
10
11
// Run attaches the router to a http.Server and starts listening and serving HTTP requests.
// It is a shortcut for http.ListenAndServe(addr, router)
// Note: this method will block the calling goroutine indefinitely unless an error happens.
func (engine *Engine) Run(addr ...string) (err error) {
defer func() { debugPrintError(err) }()

address := resolveAddress(addr) // addr 是动态参数,默认值取:8080
debugPrint("Listening and serving HTTP on %s\n", address)
err = http.ListenAndServe(address, engine)
return
}
  1. engine.ServeHTTP到底干了啥? Engine结构体的方法集:
    gin_Engine_methods

gin.Context  

1
2
3
4
5
6
7
8
9
10
11
12
13
// ServeHTTP conforms to the http.Handler interface.
func (engine *Engine) ServeHTTP(w http.ResponseWriter, req *http.Request) {
// 源码#Line145行定义,这里返回engine.allocateContext()的结果
// 是*gin.Context指针
c := engine.pool.Get().(*Context) // 从连接池中取出一个连接
c.writermem.reset(w) // 重置 http.responseWriter
c.Request = req
c.reset() // 重置Context

engine.handleHTTPRequest(c) // 核心!!! 路由处理逻辑

engine.pool.Put(c) // 执行结束,将连接放入连接池
}
  1. engine.handleHTTPRequest(c)的细节。

engine_handleHTTPRequest

可以看到源码#Line403行调用c.Next()c.index从-1自增到0,然后调用c.handlers[0]句柄,执行第一个中间件RouteLogger,而在中间件中我们需要再次调用c.Next()。非常明显的一个递归调用,然后执行第二个中间件RecoverWithWriter,之后调用GET动词注册的路由api.Ping,最后调用链路依次返回。
参考下图(点击可放大)

gin_Route_Next

路由

Gin的路由按HTTP动词,分9组(默认engine.trees = make(methodTrees, 0, 9))分别对应GET组,POST组,PUT组等。methodTrees[]methodTree的别名:type methodTrees []methodTree
node是一颗前缀树或Radix trie

1
2
3
4
type methodTree struct {
method string // 即HTTP动词,如GET
root *node // 路由链路
}

Trie

trie译为字典树或单词查找树或前缀树。这是一种搜索树——存储动态集合或关联数组的有序的树形数据结构,且通常使用字符串做键。与二叉搜索树不同,其节点上并不直接存键。其在树中的位置决定了与其关联的键。所有的子节点都有相同的前缀,而根节点对应的是空字符串。键只与叶子节点关联。

trie术语的发明者念/ˈtriː/(tree),而有些作者念为/ˈtraɪ/以便和tree区别。

下图是一颗字典树,描述了键值为Atoteatedteniininn的情况。(图中节点并不是完全有序的,虽然应该如此:如root节点与t节点)
wiki字典树

不难想象,字典树典型的应用场景是单词计数。

trie通常用来取代hash table,因为有如下优势:

  • 在最坏的情况下,trie的时间复杂度是O(m),其中m是字符串的长度。但哈希表有key碰撞的情况,最坏的情况下其复杂度是O(N),虽然通常是O(1),且计算哈希的复杂度是O(m)
  • trie中没有碰撞。
  • trie中一个key对应多个值时,会使用buckets来存储多个值,与哈希表中发生碰撞时使用的桶相似。
  • 不论有多少个key,都不需哈希函数或重哈希函数。
  • key的路径是有序的。

但同时,相对哈希表,trie有如下缺点:

  • trie的搜索通常比哈希表慢,特别是需要从硬盘上加载数据时。
  • 浮点数做key通常导致链路过长。
  • 有些trie可能比哈希表需要更多的空间,因为每一个字符都要分配内存。而哈希表只需要申请一块内存。
trie_001

Radix Tree

radix tree也叫radix triecompact prefix trie。在字典树中,每一个字符都要占一个节点,这样造成树过高。radix trie则将唯一的子节点压缩到自身来降低树的高度。


参考资料:

  1. 字典树
  2. Radix树
  3. Trie Data Structure Tutorial - Introduction to the Trie Data Structure
  4. Trie and Patricia Trie Overview
  5. 图解Redis中的Radix树
  6. Linux 内核数据结构:Radix树

解析请求参数

渲染JSON

session & cookie

URL重定向

goroutin inside a middleware

日志模块

debug日志

/debug.go#L55

1
2
3
4
5
6
7
8
9
10
11
func debugPrint(format string, values ...interface{}) {
if IsDebugging() {
if !strings.HasSuffix(format, "\n") {
format += "\n"
}
// DefaultWriter是在项目bootstrap阶段配置的写句柄
// 可以通过DefaultWriter=io.MultiWriter(...)自定义
// 也可以使用默认值os.Stdout见/mode.go#L31-38
fmt.Fprintf(DefaultWriter, "[GIN-debug] "+format, values...)
}
}

/debug.go#L97

1
2
3
4
5
6
7
8
func debugPrintError(err error) {
if err != nil {
if IsDebugging() {
// DefaultErrorWriter is the default io.Writer used by Gin to debug errors
fmt.Fprintf(DefaultErrorWriter, "[GIN-debug] [ERROR] %v\n", err)
}
}
}

路由日志

/logger.go#L131

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
// defaultLogFormatter is the default log format function Logger middleware uses.
var defaultLogFormatter = func(param LogFormatterParams) string {
var statusColor, methodColor, resetColor string
if param.IsOutpu123456
or() {
statusColor = param.StatusCodeColor()
methodColor = param.MethodColor()
resetColor = param.ResetColor()
}

if param.Latency > time.Minute {
// Truncate in a golang < 1.8 safe way
param.Latency = param.Latency - param.Latency%time.Second
}
// 默认日志格式:
// [GIN] 时间戳|HTTP_Code|响应时间|客户IP| http_verb url 错误信息
return fmt.Sprintf("[GIN] %v |%s %3d %s| %13v | %15s |%s %-7s %s %#v\n%s",
param.TimeStamp.Format("2006/01/02 - 15:04:05"),
statusColor, param.StatusCode, resetColor,
param.Latency,
param.ClientIP,
methodColor, param.Method, resetColor,
param.Path,
param.ErrorMessage,
)
}

/logger.go#L203

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
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
// LoggerWithConfig instance a Logger middleware with config.
func LoggerWithConfig(conf LoggerConfig) HandlerFunc {
formatter := conf.Formatter
if formatter == nil {
formatter = defaultLogFormatter
}

out := conf.Output
if out == nil {
out = DefaultWriter
}

notlogged := conf.SkipPaths

isTerm := true

if w, ok := out.(*os.File); !ok || os.Getenv("TERM") == "dumb" ||
(!isatty.IsTerminal(w.Fd()) && !isatty.IsCygwinTerminal(w.Fd())) {
isTerm = false
}

var skip map[string]struct{}

if length := len(notlogged); length > 0 {
skip = make(map[string]struct{}, length)

for _, path := range notlogged {
skip[path] = struct{}{}
}
}

return func(c *Context) {
// Start timer
start := time.Now()
path := c.Request.URL.Path
raw := c.Request.URL.RawQuery

// Process request
c.Next()

// Log only when path is not being skipped
if _, ok := skip[path]; !ok {
param := LogFormatterParams{
Request: c.Request,
isTerm: isTerm,
Keys: c.Keys,
}

// Stop timer
param.TimeStamp = time.Now()
param.Latency = param.TimeStamp.Sub(start)

param.ClientIP = c.ClientIP()
param.Method = c.Request.Method
param.StatusCode = c.Writer.Status()
param.ErrorMessage = c.Errors.ByType(ErrorTypePrivate).String()

param.BodySize = c.Writer.Size()

if raw != "" {
path = path + "?" + raw
}

param.Path = path

fmt.Fprint(out, formatter(param))
}
}
}

Build a single binary with templates

See a complete example in the https://github.com/gin-gonic/examples/tree/master/assets-in-binary directory.

http2 server push

https on port 8080

go服务要不要配nginx前端


参考阅读:

  1. Gin的路由为什么这么快?

参考资料: