持续整理中,不适合阅读
GIN版本 commithash a71af9c144f9579f6dbe945341c1df37aaf09c0d Gin框架的特点
快:路由使用radix trie
实现,检索路径短。无反射。API性能可预测。
支持中间件:请求可以有多个中间件逐个处理,最后交给业务处理。例如:Logger
,Authorization
,GZIP
,最后写入数据库。
若发生了panic
,Gin可以捕获并恢复错误,因此服务并不会终止,且可有机会介入错误恢复的过程。
JSON校验:Gin可以解析并校验请求的json数据,例如检查字段值。
路由分组:更好的组织路由。通过分组将需要鉴权和不需鉴权的路由分开,分组可以无限嵌套且不影响性能。
错误管理:Gin可以和很方便的收集错误信息。最后使用中间件将错误写入文件或数据库或发送到网络上。
内置视图渲染:提供了易用的接口来渲染JSON
,XML
和HTML
。
可扩展:自定义中间件非常容易。
源代码阅读 服务启动 Socket Server VS HTTP Server HTTP是应用层协议 ;Socket是系统提供的抽象接口,它直接操作传输层协议(如TCP
、UDP
等)来工作。它们不是一个层级上的概念。 所以,只要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] 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的实现里发现
创建连接net.Listen
网络监听srv.Accept()
读取数据c.Read(buf)
额外的,在服务端发送完数据后,应该要关闭连接
带着以上四个目标,我们来跟一下HTTP Server的启动过程。
启动HTTP Servererr := http.ListenAndServe("127.0.0.1:8080", handler{})
构造server对象
1 2 3 4 func ListenAndServe (addr string , handler Handler) error { server := &Server{Addr: addr, Handler: handler} return server.ListenAndServe() }
调用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) }
跟入#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) go c.serve(connCtx) } }
继续挖go c.serve(connCtx)
看看net/http
是如何处理一个Request的。先快速扫一下这个函数里面做了哪些事情:
#Line20w, err := c.readRequest(ctx)
构建Response对象。向内追找到HTTP协议的解析过程newTextprotoReader
。目标3找到 。
#Line35serverHandler{c.server}.ServeHTTP(w, w.req)
处理业务逻辑(即用户定义的路由逻辑)。ServeHTTP
的第一个参数w
就是Response对象,负责向客户端响应数据,w.req
即Request,负责解析请求参数、头信息等。
#Line40w.finishRequest()
中有flush操作,到这里服务器已经完成了数据响应。
#Line50-64处理了keep-alive
重用连接和idle_timeout
空闲超时断开连接的逻辑。这里涉及到一些网络知识不具体展开。 若设置了Connection: close
或者服务器保持连接直到空闲超时,都会return从而执行#Line5中的defer代码,注意源代码中的#Line1775~1777 。目标4找到
需要额外关注一下#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 func (c *conn) serve(ctx context.Context) { c.remoteAddr = c.rwc.RemoteAddr().String() ctx = context.WithValue(ctx, LocalAddrContextKey, c.rwc.LocalAddr()) defer func () {...}() ... 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() { c.setState(c.rwc, StateActive) } if err != nil {...} 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() { 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 Server
demo中#Line4-9的代码。
Gin的启动过程 挖完了net/http
包,对http网络请求的过程有了一个整体的认知,接下来正式开挖Gin。
启动服务非常简便engine := gin.New()
然后engine.Run(":8080")
1 2 3 4 5 6 7 8 9 func main () { engine := gin.New() ... engine.Run(":8080" ) }
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 func New () *Engine { debugPrintWARNINGNew() 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 }
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 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 }
engine.ServeHTTP
到底干了啥? Engine
结构体的方法集:
gin.Context
1 2 3 4 5 6 7 8 9 10 11 12 13 func (engine *Engine) ServeHTTP(w http.ResponseWriter, req *http.Request) { c := engine.pool.Get().(*Context) c.writermem.reset(w) c.Request = req c.reset() engine.handleHTTPRequest(c) engine.pool.Put(c) }
engine.handleHTTPRequest(c)
的细节。
可以看到源码#Line403行调用c.Next()
后c.index
从-1自增到0,然后调用c.handlers[0]
句柄,执行第一个中间件RouteLogger
,而在中间件中我们需要再次调用c.Next()
。非常明显的一个递归调用,然后执行第二个中间件RecoverWithWriter
,之后调用GET
动词注册的路由api.Ping
,最后调用链路依次返回。 参考下图(点击可放大)
路由 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 root *node }
Trie
trie
译为字典树或单词查找树或前缀树。这是一种搜索树——存储动态集合或关联数组的有序的树形数据结构,且通常使用字符串做键。与二叉搜索树不同,其节点上并不直接存键。其在树中的位置决定了与其关联的键。所有的子节点都有相同的前缀,而根节点对应的是空字符串。键只与叶子节点关联。
trie
术语的发明者念/ˈtriː/
(tree),而有些作者念为/ˈtraɪ/
以便和tree区别。
下图是一颗字典树,描述了键值为A
、to
、tea
、ted
、ten
、i
、in
、inn
的情况。(图中节点并不是完全有序的,虽然应该如此:如root节点与t
节点)
不难想象,字典树典型的应用场景是单词计数。
trie
通常用来取代hash table
,因为有如下优势:
在最坏的情况下,trie
的时间复杂度是O(m)
,其中m是字符串的长度。但哈希表有key
碰撞的情况,最坏的情况下其复杂度是O(N)
,虽然通常是O(1)
,且计算哈希的复杂度是O(m)
。
trie
中没有碰撞。
当trie
中一个key
对应多个值时,会使用buckets
来存储多个值,与哈希表中发生碰撞时使用的桶相似。
不论有多少个key
,都不需哈希函数或重哈希函数。
key
的路径是有序的。
但同时,相对哈希表,trie
有如下缺点:
trie
的搜索通常比哈希表慢,特别是需要从硬盘上加载数据时。
浮点数做key
通常导致链路过长。
有些trie
可能比哈希表需要更多的空间,因为每一个字符都要分配内存。而哈希表只需要申请一块内存。
Radix Tree
radix tree
也叫radix trie
或compact prefix trie
。在字典树中,每一个字符都要占一个节点,这样造成树过高。radix trie
则将唯一的子节点压缩到自身来降低树的高度。
参考资料:
字典树
Radix树
Trie Data Structure Tutorial - Introduction to the Trie Data Structure
Trie and Patricia Trie Overview
图解Redis中的Radix树
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" } 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() { 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 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 { param.Latency = param.Latency - param.Latency%time.Second } 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 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 := time.Now() path := c.Request.URL.Path raw := c.Request.URL.RawQuery c.Next() if _, ok := skip[path]; !ok { param := LogFormatterParams{ Request: c.Request, isTerm: isTerm, Keys: c.Keys, } 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前端
参考阅读:
Gin的路由为什么这么快?
参考资料: