gin是作为golang web开发中被广泛使用到的框架,了解其内部的实现有助于我们更好地理解gin的设计思想。

这篇文章主要探讨两个问题。

  • http请求如何流转到gin
  • gin为什么比golang的http路由寻找更快

开始之前我们先来看看分别用golang原生的http包实现一个http服务和使用gin实现的代码,先看看原生http包实现的http服务

package mainimport ("net/http"
)func main() {http.HandleFunc("/ping", func(writer http.ResponseWriter, request *http.Request) {writer.Write([]byte(`{"message":"ok"}`))})http.ListenAndServe(":9090", nil)
}

这段代码做了两件事情,注册路由、启动服务监听9090端口。接下来我们对这段代码进一步分析,在第8行的地方是将路由/ping和对应的处理函数注册到http服务中,我们进入http.HandleFunc()函数看看该函数做了什么事情。

// HandleFunc registers the handler function for the given pattern
// in the DefaultServeMux.
// The documentation for ServeMux explains how patterns are matched.
func HandleFunc(pattern string, handler func(ResponseWriter, *Request)) {DefaultServeMux.HandleFunc(pattern, handler)
}

将路由和处理函数注册到了DefaultServeMux中,所以我们先看看DefaultServeMux的结构是什么。

type ServeMux struct {mu    sync.RWMutexm     map[string]muxEntryes    []muxEntry // slice of entries sorted from longest to shortest.hosts bool       // whether any patterns contain hostnames
}type muxEntry struct {h       Handlerpattern string
}// NewServeMux allocates and returns a new ServeMux.
func NewServeMux() *ServeMux { return new(ServeMux) }// DefaultServeMux is the default ServeMux used by Serve.
var DefaultServeMux = &defaultServeMuxvar defaultServeMux ServeMux

第17行代码就是刚刚用来注册http路由的服务,通过第19行代码知道了他是一个ServeMux类型。知道了DefaultServeMux的类型我们接着看具体的实现代码。

// HandleFunc registers the handler function for the given pattern.
func (mux *ServeMux) HandleFunc(pattern string, handler func(ResponseWriter, *Request)) {if handler == nil {panic("http: nil handler")}mux.Handle(pattern, HandlerFunc(handler))
}// Handle registers the handler for the given pattern.
// If a handler already exists for pattern, Handle panics.
func (mux *ServeMux) Handle(pattern string, handler Handler) {mux.mu.Lock()defer mux.mu.Unlock()if pattern == "" {panic("http: invalid pattern")}if handler == nil {panic("http: nil handler")}if _, exist := mux.m[pattern]; exist {panic("http: multiple registrations for " + pattern)}if mux.m == nil {mux.m = make(map[string]muxEntry)}e := muxEntry{h: handler, pattern: pattern}mux.m[pattern] = eif pattern[len(pattern)-1] == '/' {mux.es = appendSorted(mux.es, e)}if pattern[0] != '/' {mux.hosts = true}
}

主要的代码就是第29行,这里将路由和处理函数保存在了ServeMux的m中,通过前面的代码我们知道m是一个map,到这里路由注册的过程就分析完了。接下来我们来看看 http.ListenAndServe()做了什么事情。

// ListenAndServe listens on the TCP network address srv.Addr and then
// calls Serve to handle requests on incoming connections.
// Accepted connections are configured to enable TCP keep-alives.
//
// If srv.Addr is blank, ":http" is used.
//
// ListenAndServe always returns a non-nil error. After Shutdown or Close,
// the returned error is ErrServerClosed.
func (srv *Server) ListenAndServe() error {if srv.shuttingDown() {return ErrServerClosed}addr := srv.Addrif addr == "" {addr = ":http"}ln, err := net.Listen("tcp", addr)if err != nil {return err}return srv.Serve(ln)
}

第22行就是真正开始启动http服务,并接受请求的函数。第17行创建了主动套接字并监听套接字,接着我们进入Serve()函数。

func (srv *Server) Serve(l net.Listener) error {if fn := testHookServerServe; fn != nil {fn(srv, l) // call hook with unwrapped listener}origListener := ll = &onceCloseListener{Listener: l}defer l.Close()if err := srv.setupHTTP2_Serve(); err != nil {return err}if !srv.trackListener(&l, true) {return ErrServerClosed}defer srv.trackListener(&l, false)baseCtx := context.Background()if srv.BaseContext != nil {baseCtx = srv.BaseContext(origListener)if baseCtx == nil {panic("BaseContext returned a nil context")}}var tempDelay time.Duration // how long to sleep on accept failurectx := context.WithValue(baseCtx, ServerContextKey, srv)for {rw, err := l.Accept()if err != nil {select {case <-srv.getDoneChan():return ErrServerCloseddefault:}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 := ctxif cc := srv.ConnContext; cc != nil {connCtx = cc(connCtx, rw)if connCtx == nil {panic("ConnContext returned nil")}}tempDelay = 0c := srv.newConn(rw)c.setState(c.rwc, StateNew, runHooks) // before Serve can returngo c.serve(connCtx)}
}

比较关键的几行代码是第31行和第61行,他们做的事情分别是接收到请求并解析请求数据,使用新的goroutines处理该请求。接着我们需要看看golang具体是如何处理接收到的请求

// 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() {if err := recover(); err != nil && err != ErrAbortHandler {const size = 64 << 10buf := make([]byte, size)buf = buf[:runtime.Stack(buf, false)]c.server.logf("http: panic serving %v: %v\n%s", c.remoteAddr, err, buf)}if !c.hijacked() {c.close()c.setState(c.rwc, StateClosed, runHooks)}}()if tlsConn, ok := c.rwc.(*tls.Conn); ok {if d := c.server.ReadTimeout; d > 0 {c.rwc.SetReadDeadline(time.Now().Add(d))}if d := c.server.WriteTimeout; d > 0 {c.rwc.SetWriteDeadline(time.Now().Add(d))}if err := tlsConn.HandshakeContext(ctx); err != nil {// If the handshake failed due to the client not speaking// TLS, assume they're speaking plaintext HTTP and write a// 400 response on the TLS conn's underlying net.Conn.if re, ok := err.(tls.RecordHeaderError); ok && re.Conn != nil && tlsRecordHeaderLooksLikeHTTP(re.RecordHeader) {io.WriteString(re.Conn, "HTTP/1.0 400 Bad Request\r\n\r\nClient sent an HTTP request to an HTTPS server.\n")re.Conn.Close()return}c.server.logf("http: TLS handshake error from %s: %v", c.rwc.RemoteAddr(), err)return}c.tlsState = new(tls.ConnectionState)*c.tlsState = tlsConn.ConnectionState()if proto := c.tlsState.NegotiatedProtocol; validNextProto(proto) {if fn := c.server.TLSNextProto[proto]; fn != nil {h := initALPNRequest{ctx, tlsConn, serverHandler{c.server}}// Mark freshly created HTTP/2 as active and prevent any server state hooks// from being run on these connections. This prevents closeIdleConns from// closing such connections. See issue https://golang.org/issue/39776.c.setState(c.rwc, StateActive, skipHooks)fn(c.server, tlsConn, h)}return}}// HTTP/1.x from here on.ctx, cancelCtx := context.WithCancel(ctx)c.cancelCtx = cancelCtxdefer 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, runHooks)}if err != nil {const errorHeaders = "\r\nContent-Type: text/plain; charset=utf-8\r\nConnection: close\r\n\r\n"switch {case err == errTooLarge:// Their HTTP client may or may not be// able to read this if we're// responding to them and hanging up// while they're still writing their// request. Undefined behavior.const publicErr = "431 Request Header Fields Too Large"fmt.Fprintf(c.rwc, "HTTP/1.1 "+publicErr+errorHeaders+publicErr)c.closeWriteAndWait()returncase isUnsupportedTEError(err):// Respond as per RFC 7230 Section 3.3.1 which says,//      A server that receives a request message with a//      transfer coding it does not understand SHOULD//      respond with 501 (Unimplemented).code := StatusNotImplemented// We purposefully aren't echoing back the transfer-encoding's value,// so as to mitigate the risk of cross side scripting by an attacker.fmt.Fprintf(c.rwc, "HTTP/1.1 %d %s%sUnsupported transfer encoding", code, StatusText(code), errorHeaders)returncase isCommonNetReadError(err):return // don't replydefault:if v, ok := err.(statusError); ok {fmt.Fprintf(c.rwc, "HTTP/1.1 %d %s: %s%s%d %s: %s", v.code, StatusText(v.code), v.text, errorHeaders, v.code, StatusText(v.code), v.text)return}publicErr := "400 Bad Request"fmt.Fprintf(c.rwc, "HTTP/1.1 "+publicErr+errorHeaders+publicErr)return}}// Expect 100 Continue supportreq := w.reqif req.expectsContinue() {if req.ProtoAtLeast(1, 1) && req.ContentLength != 0 {// Wrap the Body reader with one that replies on the connectionreq.Body = &expectContinueReader{readCloser: req.Body, resp: w}w.canWriteContinue.setTrue()}} else if req.Header.get("Expect") != "" {w.sendExpectationFailed()return}c.curReq.Store(w)if requestBodyRemains(req.Body) {registerOnHitEOF(req.Body, w.conn.r.startBackgroundRead)} else {w.conn.r.startBackgroundRead()}// HTTP cannot have multiple simultaneous active requests.[*]// Until the server replies to this request, it can't read another,// so we might as well run the handler in this goroutine.// [*] Not strictly true: HTTP pipelining. We could let them all process// in parallel even if their responses need to be serialized.// But we're not going to implement HTTP pipelining because it// was never deployed in the wild and the answer is HTTP/2.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, runHooks)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{})}
}

关键的代码是第137行将需要返回的response和reques

func (sh serverHandler) ServeHTTP(rw ResponseWriter, req *Request) {handler := sh.srv.Handlerif handler == nil {handler = DefaultServeMux}if req.RequestURI == "*" && req.Method == "OPTIONS" {handler = globalOptionsHandler{}}if req.URL != nil && strings.Contains(req.URL.RawQuery, ";") {var allowQuerySemicolonsInUse int32req = req.WithContext(context.WithValue(req.Context(), silenceSemWarnContextKey, func() {atomic.StoreInt32(&allowQuerySemicolonsInUse, 1)}))defer func() {if atomic.LoadInt32(&allowQuerySemicolonsInUse) == 0 {sh.srv.logf("http: URL query contains semicolon, which is no longer a supported separator; parts of the query may be stripped when parsed; see golang.org/issue/25192")}}()}handler.ServeHTTP(rw, req)
}

在这里我们终于又和注册路由时候使用的DefaultServeMux见面了,因为在启动服务的时候handler传入的是nil,所以这里默认的使用DefaultServeMux,然而此时的DefaultServeMux已经包含了注册的路由。接下来我们来看看DefaultServeMux的ServeHTTP()是如何实现的。

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

第11行就是通过通过请求中的路由再返回路由对应的处理函数

func (mux *ServeMux) Handler(r *Request) (h Handler, pattern string) {// CONNECT requests are not canonicalized.if r.Method == "CONNECT" {// If r.URL.Path is /tree and its handler is not registered,// the /tree -> /tree/ redirect applies to CONNECT requests// but the path canonicalization does not.if u, ok := mux.redirectToPathSlash(r.URL.Host, r.URL.Path, r.URL); ok {return RedirectHandler(u.String(), StatusMovedPermanently), u.Path}return mux.handler(r.Host, r.URL.Path)}// All other requests have any port stripped and path cleaned// before passing to mux.handler.host := stripHostPort(r.Host)path := cleanPath(r.URL.Path)// If the given path is /tree and its handler is not registered,// redirect for /tree/.if u, ok := mux.redirectToPathSlash(host, path, r.URL); ok {return RedirectHandler(u.String(), StatusMovedPermanently), u.Path}if path != r.URL.Path {_, pattern = mux.handler(host, path)u := &url.URL{Path: path, RawQuery: r.URL.RawQuery}return RedirectHandler(u.String(), StatusMovedPermanently), pattern}return mux.handler(host, r.URL.Path)
}

第32行然后接着往下走

func (mux *ServeMux) handler(host, path string) (h Handler, pattern string) {mux.mu.RLock()defer mux.mu.RUnlock()// Host-specific pattern takes precedence over generic onesif mux.hosts {h, pattern = mux.match(host + path)}if h == nil {h, pattern = mux.match(path)}if h == nil {h, pattern = NotFoundHandler(), ""}return
}

第7行

func (mux *ServeMux) match(path string) (h Handler, pattern string) {// Check for exact match first.v, ok := mux.m[path]if ok {return v.h, v.pattern}// Check for longest valid match.  mux.es contains all patterns// that end in / sorted from longest to shortest.for _, e := range mux.es {if strings.HasPrefix(path, e.pattern) {return e.h, e.pattern}}return nil, ""
}

第3~5行,如请求的路由有对应的处理函数则返回对应的处理函数。得到了对应的处理函数,然后调用处理函数实现的ServeHTTP()的逻辑

// The HandlerFunc type is an adapter to allow the use of
// ordinary functions as HTTP handlers. If f is a function
// with the appropriate signature, HandlerFunc(f) is a
// Handler that calls f.
type HandlerFunc func(ResponseWriter, *Request)// ServeHTTP calls f(w, r).
func (f HandlerFunc) ServeHTTP(w ResponseWriter, r *Request) {f(w, r)
}

通过刚开始注册路由的时候我们传入的处理函数是HandlerFunc类型,而且对应的ServeHTTP()逻辑是运行处理函数,所以到这里逻辑就走到了我们的业务逻辑了,这就是使用golang原生http包实现的http服务具体的实现过程。

接着我们来看看gin的http服务有什么不同,gin中匹配路由和处理函数的的数据结构是Radix Tree,这是前缀树的优化方案

func (engine *Engine) addRoute(method, path string, handlers HandlersChain) {assert1(path[0] == '/', "path must begin with '/'")assert1(method != "", "HTTP method can not be empty")assert1(len(handlers) > 0, "there must be at least one handler")debugPrintRoute(method, path, handlers)root := engine.trees.get(method)if root == nil {root = new(node)root.fullPath = "/"engine.trees = append(engine.trees, methodTree{method: method, root: root})}root.addRoute(path, handlers)// Update maxParamsif paramsCount := countParams(path); paramsCount > engine.maxParams {engine.maxParams = paramsCount}if sectionsCount := countSections(path); sectionsCount > engine.maxSections {engine.maxSections = sectionsCount}
}

第14行向该树添加节点,gin中每一个http请求方法都单独维护了一棵Radix Tree。接着我们看Run()函数做了什么事情

func (engine *Engine) Run(addr ...string) (err error) {defer func() { debugPrintError(err) }()if engine.isUnsafeTrustedProxies() {debugPrint("[WARNING] You trusted all proxies, this is NOT safe. We recommend you to set a value.\n" +"Please check https://pkg.go.dev/github.com/gin-gonic/gin#readme-don-t-trust-all-proxies for details.")}address := resolveAddress(addr)debugPrint("Listening and serving HTTP on %s\n", address)err = http.ListenAndServe(address, engine)return
}

第11行将我们将建的gin实例作为handler传入ListenAndServe,之后的逻辑就是http包原生的逻辑,唯一不同的是最后调用的ServeHTTP是gin的实现而不是DefaultServeMux的实现接下来我们看看gin的ServeHTTP实现

func (engine *Engine) ServeHTTP(w http.ResponseWriter, req *http.Request) {c := engine.pool.Get().(*Context)c.writermem.reset(w)c.Request = reqc.reset()engine.handleHTTPRequest(c)engine.pool.Put(c)
}

gin将请求包装成Context然后调用handleHTTPRequest在Radix Tree找到路由对应的处理函数,并调用该函函数。

func (engine *Engine) handleHTTPRequest(c *Context) {httpMethod := c.Request.MethodrPath := c.Request.URL.Pathunescape := falseif engine.UseRawPath && len(c.Request.URL.RawPath) > 0 {rPath = c.Request.URL.RawPathunescape = engine.UnescapePathValues}if engine.RemoveExtraSlash {rPath = cleanPath(rPath)}// Find root of the tree for the given HTTP methodt := engine.treesfor i, tl := 0, len(t); i < tl; i++ {if t[i].method != httpMethod {continue}root := t[i].root// Find route in treevalue := root.getValue(rPath, c.params, c.skippedNodes, unescape)if value.params != nil {c.Params = *value.params}if value.handlers != nil {c.handlers = value.handlersc.fullPath = value.fullPathc.Next()c.writermem.WriteHeaderNow()return}if httpMethod != "CONNECT" && rPath != "/" {if value.tsr && engine.RedirectTrailingSlash {redirectTrailingSlash(c)return}if engine.RedirectFixedPath && redirectFixedPath(c, root, engine.RedirectFixedPath) {return}}break}if engine.HandleMethodNotAllowed {for _, tree := range engine.trees {if tree.method == httpMethod {continue}if value := tree.root.getValue(rPath, nil, c.skippedNodes, unescape); value.handlers != nil {c.handlers = engine.allNoMethodserveError(c, http.StatusMethodNotAllowed, default405Body)return}}}c.handlers = engine.allNoRouteserveError(c, http.StatusNotFound, default404Body)
}

第22~31行获取处理函数,并执行中间件和处理函数。

到这里我们就一起知道了http请求是如何从golang流转到gin的,只要我们自己定义的结构体实现了ServeHTTP函数并在启动服务使用我们自己实现的handler类型的结构体,那么最后就会流转的自定义的http handler。

通过分析我们知道原生的DefaultServeMux路由和处理函数对应关系使用的是map,而gin使用的是Radix Tree,所以gin比原生http快的原因就是这两种数据结构的的性能差别,map在最糟糕的条件下时间复杂度会变成O(n)也就是所有的key hash只有相同,最后变成链表。而且由于map的性质,所有的key不是很可能不是连续的,所有可能造成空间浪费。

关于gin的学习今天就到这里,有什么错误的地方希望指正。

微信公众号:

golang如何将http请求流转到gin相关推荐

  1. golang微信公众号请求获取信息

    初次用golang在公众号中获取信息,记录一下 看了下文档,粗略的写了个demo,如下: func HttpGet(c*gin.Context) {var param GetTypeif er:=c. ...

  2. golang:解析HTTP请求参数

    <GO程序设计语言>设计中案例,仅作为笔记进行收藏.此案例将HTTP请求参数解析成对应的匿名结构体,并使用反射来获取字段标签. params 解析函数 package paramsimpo ...

  3. golang post发送 json请求

    实例1: package mainimport ("bytes""fmt""io/ioutil""net/http" ) ...

  4. Go框架 gin 源码学习--路由的实现原理剖析

    往期回顾: gin源码解析 - gin 与 net/http 的关系 gin 源码解析 - 详解http请求在gin中的流转过程 上面两篇文章基本讲清楚了 Web Server 如何接收客户端请求,以 ...

  5. gin 源码解析 - 详解http请求在gin中的流转过程

    本篇文章是 gin 源码分析系列的第二篇,这篇文章我们主要弄清一个问题:一个请求通过 net/http 的 socket 接收到请求后, 是如何回到 gin 中处理逻辑的? 我们仍然以 net/htt ...

  6. Go语言(Golang)的Web框架比较:gin VS echo

    Go语言(Golang)的web框架比较之:gin vs echo 由 butaixianran 在 2016-01-23 22:00 发布 35423 次点击 原文发在:https://771dia ...

  7. [转]Go语言(Golang)的Web框架比较:gin VS echo

    Go语言(Golang)的web框架比较之:gin vs echo 由 butaixianran 在 2016-01-23 22:00 发布 35423 次点击 原文发在:https://771dia ...

  8. 使用Golang、Gin和React、esbuild开发的Blog

    作者:元亮   360奇舞团工程师 本指北手册,手把手跟大家从头开始构建一个完成一个Go作为服务的Web应用程序 - Blog 完整的应用程序 可以在 github上下载 [1] Go(Golang) ...

  9. Golang库学习笔记 Gin(三)

    快速入门 今天,我们将要基于一个例子,学习如何使用GIN框架. 目录 文章目录 快速入门 目录 要求 安装 1.下载并安装 gin: 2.将 gin 引入到代码中: 3.(可选)如果使用诸如 http ...

最新文章

  1. 数学建模第五节2020.5.8-17补
  2. 1224 哥德巴赫猜想(2)
  3. 现有类 成 mfc类_女人不想成“黄脸婆”,4类食物是衰老“催化剂”,女人尽量远离_氧化...
  4. ❤️六万字《SpringMVC框架介绍—从入门到高级》(建议收藏)❤️
  5. 开源项目管理系统:ProjectForge
  6. Symbols andSymbol Tables
  7. (转) Lua使用心得一
  8. cad统计面积长度插件vlx_CAD线段长度计算插件
  9. win7怎么看计算机Mac地址,win7如何查看mac地址?win7系统查看mac地址两种方法
  10. 搭建FTP站点(Windows)
  11. 人脸识别帧数太低的解决方法
  12. 一款智能家居APP的雏形
  13. HTML超链接实现页面内跳转
  14. 最全的monkey测试过程及分析
  15. hash路由实现微信登陆后的重定向
  16. 字节跳动李航入选ACL Fellow,他曾这样看待机器学习
  17. 【WIN32APIDAPI】RegisterClass CreateWindowEx UpdateWindow
  18. 太赫兹在医学诊断方面的应用
  19. 了解CV和RoboMaster视觉组(四)视觉组使用的硬件
  20. hexo加Github搭建个人博客(一、二)

热门文章

  1. 决定高薪的细节守则 2012_07_28
  2. VSC配置C C++
  3. -- 38、查询课程编号为01且课程成绩在80分以上的学生的学号和姓名
  4. 浅谈2017棋牌游戏的前景 运营 推广(上) 转贴
  5. 开发需要的软件-Java
  6. i春秋 XSS闯关 wp
  7. GD32F10x的侵入检测事件
  8. 零基础入门--中文实体关系抽取(BiLSTM+attention,含代码)
  9. 【GD32F427开发板试用】06-硬件I2C软件I2C驱动0.91OLED
  10. 无人便利店沿用超高频RFID技术将快速布局全国