<kbd id="afajh"><form id="afajh"></form></kbd>
<strong id="afajh"><dl id="afajh"></dl></strong>
    <del id="afajh"><form id="afajh"></form></del>
        1. <th id="afajh"><progress id="afajh"></progress></th>
          <b id="afajh"><abbr id="afajh"></abbr></b>
          <th id="afajh"><progress id="afajh"></progress></th>

          gin 源碼閱讀之路由的實現(xiàn)剖析

          共 10229字,需瀏覽 21分鐘

           ·

          2021-10-02 04:01

          上面兩篇文章基本講清楚了 Web Server 如何接收客戶端請求,以及如何將請求流轉(zhuǎn)到 gin 的邏輯。

          gin 原理剖析說到這里,就完全進入 gin 的邏輯里面了。gin 已經(jīng)拿到 http 請求了,第一件重要的事情肯定就是重寫路由了,所以本節(jié)內(nèi)容主要是分析 gin 的路由相關(guān)的內(nèi)容。

          其實 gin 的路由也不是完全自己寫的,其實很重要的一部分代碼是使用的開源的 julienschmidt/httprouter,當(dāng)然 gin 也添加了部分自己獨有的功能,如:routergroup。

          什么是路由?

          這個其實挺容易理解的,就是根據(jù)不同的 URL 找到對應(yīng)的處理函數(shù)即可。

          目前業(yè)界 Server 端 API 接口的設(shè)計方式一般是遵循 RESTful 風(fēng)格的規(guī)范。當(dāng)然我也見過某些大公司為了降低開發(fā)人員的心智負擔(dān)和學(xué)習(xí)成本,接口完全不區(qū)分 GET/POST/DELETE 請求,完全靠接口的命名來表示。

          舉個簡單的例子,如:"刪除用戶"

          RESTful:    DELETE  /user/hhf
          No RESTful: GET     /deleteUser?name=hhf

          這種 No RESTful 的方式,有的時候確實減少一些溝通問題和學(xué)習(xí)成本,但是只能內(nèi)部使用了。這種不區(qū)分 GET/POST 的 Web 框架一般設(shè)計的會比較靈活,但是開發(fā)人員水平參差不齊,會導(dǎo)致出現(xiàn)很多“接口毒瘤”,等你發(fā)現(xiàn)的時候已經(jīng)無可奈何了,如下面這些接口:

          GET /selectUserList?userIds=[1,2,3] -> 參數(shù)是否可以是數(shù)組?
          GET /getStudentlist?skuIdCntMap={"200207366":1} -> 參數(shù)是否可以是字典?

          這樣的接口設(shè)計會導(dǎo)致開源的框架都是解析不了的,只能自己手動一層一層 decode 字符串,這里就不再詳細鋪開介紹了,等下一節(jié)說到 gin Bind 系列函數(shù)時再詳細說一下。

          繼續(xù)回到上面 RESTful 風(fēng)格的接口上面來,拿下面這些簡單的請求來說:

          GET    /user/{userID} HTTP/1.1
          POST   /user/{userID} HTTP/1.1
          PUT    /user/{userID} HTTP/1.1
          DELETE /user/{userID} HTTP/1.1

          這是比較規(guī)范的 RESTful API設(shè)計,分別代表:

          • 獲取 userID 的用戶信息
          • 更新 userID 的用戶信息(當(dāng)然還有其 json body,沒有寫出來)
          • 創(chuàng)建 userID 的用戶(當(dāng)然還有其 json body,沒有寫出來)
          • 刪除 userID 的用戶

          可以看到同樣的 URI,不同的請求 Method,最終其他代表的要處理的事情也完全不一樣。

          看到這里你可以思考一下,假如讓你來設(shè)計這個路由,要滿足上面的這些功能,你會如何設(shè)計呢?

          gin 路由設(shè)計

          如何設(shè)計不同的 Method ?

          通過上面的介紹,已經(jīng)知道 RESTful 是要區(qū)分方法的,不同的方法代表意義也完全不一樣,gin 是如何實現(xiàn)這個的呢?

          其實很簡單,不同的方法就是一棵路由樹,所以當(dāng) gin 注冊路由的時候,會根據(jù)不同的 Method 分別注冊不同的路由樹。

          GET    /user/{userID} HTTP/1.1
          POST   /user/{userID} HTTP/1.1
          PUT    /user/{userID} HTTP/1.1
          DELETE /user/{userID} HTTP/1.1

          如這四個請求,分別會注冊四顆路由樹出來。

          func (engine *Engine) addRoute(method, path string, handlers HandlersChain) {
              //....
              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)
              // ...
          }

          其實代碼也很容易看懂,

          • 拿到一個 method 方法時,去 trees slice 中遍歷
          • 如果 trees slice 存在這個 method, 則這個URL對應(yīng)的 handler 直接添加到找到的路由樹上
          • 如果沒有找到,則重新創(chuàng)建一顆新的方法樹出來, 然后將 URL對應(yīng)的 handler 添加到這個路由 樹上

          gin 路由的注冊過程

          func main() {
              r := gin.Default()
              r.GET("/ping"func(c *gin.Context) {
                  c.JSON(200, gin.H{
                      "message""pong",
                  })
              })
              r.Run() // listen and serve on 0.0.0.0:8080
          }

          這段簡單的代碼里,r.Get 就注冊了一個路由 /ping 進入 GET tree 中。這是最普通的,也是最常用的注冊方式。

          不過上面這種寫法,一般都是用來測試的,正常情況下我們會將 handler 拿到 Controller 層里面去,注冊路由放在專門的 route 管理里面,這里就不再詳細拓展,等后面具體說下 gin 的架構(gòu)分層設(shè)計。

          //controller/somePost.go
          func SomePostFunc(ctx *gin.Context) {
              // do something
              context.String(http.StatusOK, "some post done")
          }

          ```go
          // route.go
          router.POST("/somePost", controller.SomePostFunc)

          使用 RouteGroup

          v1 := router.Group("v1")
          {
              v1.POST("login"func(context *gin.Context) {
                  context.String(http.StatusOK, "v1 login")
              })
          }

          RouteGroup 是非常重要的功能,舉個例子:一個完整的 server 服務(wù),url 需要分為鑒權(quán)接口非鑒權(quán)接口,就可以使用 RouteGroup 來實現(xiàn)。其實最常用的,還是用來區(qū)分接口的版本升級。這些操作, 最終都會在反應(yīng)到gin的路由樹上

          gin 路由的具體實現(xiàn)

          func main() {
              r := gin.Default()
              r.GET("/ping"func(c *gin.Context) {
                  c.JSON(200, gin.H{
                      "message""pong",
                  })
              })
              r.Run() // listen and serve on 0.0.0.0:8080
          }

          還是從這個簡單的例子入手。我們只需要弄清楚下面三個問題即可:

          • URL->ping 放在哪里了?
          • handler-> 放在哪里了?
          • URL 和 handler 是如何關(guān)聯(lián)起來的?

          1. GET/POST/DELETE/..的最終歸宿

          func (group *RouterGroup) GET(relativePath string, handlers ...HandlerFunc) IRoutes {
             return group.handle(http.MethodGet, relativePath, handlers)
          }

          在調(diào)用POST, GET, HEAD等路由HTTP相關(guān)函數(shù)時, 會調(diào)用handle函數(shù)。handle 是 gin 路由的統(tǒng)一入口。

          // routergroup.go:L72-77
          func (group *RouterGroup) handle(httpMethod, relativePath string, handlers HandlersChain) IRoutes {
              absolutePath := group.calculateAbsolutePath(relativePath)
              handlers = group.combineHandlers(handlers)
              group.engine.addRoute(httpMethod, absolutePath, handlers)
              return group.returnObj()
          }

          2. 生成路由樹

          下面考慮一個情況,假設(shè)有下面這樣的路由,你會怎么設(shè)計這棵路由樹?

          GET /abc 
          GET /abd
          GET /af

          當(dāng)然最簡單最粗暴的就是每個字符串占用一個樹的葉子節(jié)點,不過這種設(shè)計會帶來的問題:占用內(nèi)存會升高,我們看到 abc, abd, af 都是用共同的前綴的,如果能共用前綴的話,是可以省內(nèi)存空間的。

          gin 路由樹是一棵前綴樹. 我們前面說過 gin 的每種方法(POST, GET ...)都有自己的一顆樹,當(dāng)然這個是根據(jù)你注冊路由來的,并不是一上來把每種方式都注冊一遍。gin 每棵路由大概是下面的樣子

          這個流程的代碼太多,這里就不再貼出具體代碼里,有興趣的同學(xué)可以按照這個思路看下去即可。

          3. handler 與 URL 關(guān)聯(lián)

          type node struct {
              path      string
              indices   string
              wildChild bool
              nType     nodeType
              priority  uint32
              children  []*node // child nodes, at most 1 :param style node at the end of the array
              handlers  HandlersChain
              fullPath  string
          }

          node 是路由樹的整體結(jié)構(gòu)

          • children 就是一顆樹的葉子結(jié)點。每個路由的去掉前綴后,都被分布在這些 children 數(shù)組里
          • path 就是當(dāng)前葉子節(jié)點的最長的前綴
          • handlers 里面存放的就是當(dāng)前葉子節(jié)點對應(yīng)的路由的處理函數(shù)

          當(dāng)收到客戶端請求時,如何找到對應(yīng)的路由的handler?

          《gin 源碼閱讀(2) - http請求是如何流入gin的?》第二篇說到 net/http 非常重要的函數(shù) ServeHTTP,當(dāng) server 收到請求時,必然會走到這個函數(shù)里。由于 gin 實現(xiàn)這個 ServeHTTP,所以流量就轉(zhuǎn)入 gin 的邏輯里面。

          // gin.go:L439-443
          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)
          }

          所以,當(dāng) gin 收到客戶端的請求時, 第一件事就是去路由樹里面去匹配對應(yīng)的 URL,找到相關(guān)的路由, 拿到相關(guān)的處理函數(shù)。其實這個過程就是 handleHTTPRequest 要干的事情。


          func (engine *Engine) handleHTTPRequest(c *Context) {
              // ...
              t := engine.trees
              for i, tl := 0len(t); i < tl; i++ {
                  if t[i].method != httpMethod {
                      continue
                  }
                  root := t[i].root
                  // Find route in tree
                  value := root.getValue(rPath, c.params, unescape)
                  if value.params != nil {
                      c.Params = *value.params
                  }
                  if value.handlers != nil {
                      c.handlers = value.handlers
                      c.fullPath = value.fullPath
                      c.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
              }
            // ...
          }

          從代碼上看這個過程其實也很簡單:

          • 遍歷所有的路由樹,找到對應(yīng)的方法的那棵樹
          • 匹配對應(yīng)的路由
          • 找到對應(yīng)的 handler

          總結(jié)

          說到這里,基本上把 gin 路由的整個流程說清楚了,不過關(guān)于路由樹的詳細實現(xiàn)說的比較籠統(tǒng),歡迎有興趣的同學(xué)入群詳聊(加我好友,我拉你入群)。

          寫文章不易,如果你覺得本篇文章還不錯,請大家?guī)兔?點贊、在看、分享,感謝感謝。



          推薦閱讀


          福利

          我為大家整理了一份從入門到進階的Go學(xué)習(xí)資料禮包,包含學(xué)習(xí)建議:入門看什么,進階看什么。關(guān)注公眾號 「polarisxu」,回復(fù) ebook 獲?。贿€可以回復(fù)「進群」,和數(shù)萬 Gopher 交流學(xué)習(xí)。

          瀏覽 51
          點贊
          評論
          收藏
          分享

          手機掃一掃分享

          分享
          舉報
          評論
          圖片
          表情
          推薦
          點贊
          評論
          收藏
          分享

          手機掃一掃分享

          分享
          舉報
          <kbd id="afajh"><form id="afajh"></form></kbd>
          <strong id="afajh"><dl id="afajh"></dl></strong>
            <del id="afajh"><form id="afajh"></form></del>
                1. <th id="afajh"><progress id="afajh"></progress></th>
                  <b id="afajh"><abbr id="afajh"></abbr></b>
                  <th id="afajh"><progress id="afajh"></progress></th>
                  人妻免费91| 天天爽天天摸天天爱 | 亚洲欧美日韩一级 | 最新偷拍网址 | 午夜迪级|