gin框架剖析(一)

gin框架剖析(一)

gin 是目前 Go 裡面使用最廣泛的框架之一了,弄清楚 gin 框架的原理,有助於我們更好的使用 gin。這個系列 gin 原始碼閱讀會逐步講明白 gin 的原理,歡迎關注後續文章。

gin

概覽

想弄清楚 gin, 需要弄明白以下幾個問題:

request資料是如何流轉的。

gin框架到底扮演了什麼角色。

請求從gin流入net/http, 最後又是如何回到gin中。

gin的context為何能承擔起來複雜的需求。

gin的路由演算法。

gin的中介軟體是什麼。

gin的Engine具體是個什麼東西。

net/http的requeset, response都提供了哪些有用的東西。

從gin的官方第一個demo入手:

gin框架剖析(一)

r。Run() 的原始碼:

gin框架剖析(一)

看到開始呼叫的是 http。ListenAndServe(address, engine), 這個函式是net/http的函式, 然後請求資料就在net/http開始流轉。

Request 資料是如何流轉的

先不使用gin, 直接使用net/http來處理http請求:

gin框架剖析(一)

在瀏覽器中輸入localhost:8000, 會看到Hello World。 下面利用這個簡單demo看下request的流轉流程。

HTTP是如何建立起來的

簡單的說一下http請求是如何建立起來的:(需要有基本的網路基礎, 可以找相關的書籍檢視, 推薦看UNIX網路程式設計卷1:套接字聯網API)

gin框架剖析(一)

TCP/IP 五層模型

gin框架剖析(一)

socket建立過程

在TCP/IP五層模型下, HTTP位於應用層, 需要有傳輸層來承載HTTP協議。 傳輸層比較常見的協議是TCP,UDP, SCTP等。 由於UDP不可靠, SCTP有自己特殊的運用場景, 所以一般情況下HTTP是由TCP協議承載的(可以使用wireshark抓包然後檢視各層協議)。

使用TCP協議的話, 就會涉及到TCP是如何建立起來的。 面試中能夠常遇到的名詞三次握手, 四次揮手就是在這裡產生的。 具體的建立流程就不在陳述了, 大概流程就是圖中左半邊。

所以說, 要想能夠對客戶端http請求進行迴應的話, 就首先需要建立起來TCP連線, 也就是socket。 下面要看下net/http是如何建立起來socket?

net/http 是如何建立 socket 的

從圖上可以看出, 不管server程式碼如何封裝, 都離不開bind,listen,accept這些函式。 就從上面這個簡單的demo入手檢視原始碼。

gin框架剖析(一)

註冊路由

gin框架剖析(一)

這段程式碼是在註冊一個路由及這個路由的handler到DefaultServeMux中。

gin框架剖析(一)

可以看到這個路由註冊太過簡單了, 也就給gin, iris, echo等框架留下了擴充套件的空間, 後面詳細說這個東西。

服務監聽及響應

上面路由已經註冊到net/http了, 下面就該如何建立socket了, 以及最後又如何取到已經註冊到的路由, 將正確的響應資訊從handler中取出來返回給客戶端。

1.

建立 socket

gin框架剖析(一)

2.Accept 等待客戶端連結

gin框架剖析(一)

3.提供回撥介面 ServeHTTP

gin框架剖析(一)

gin框架剖析(一)

4.回撥到實際要執行的 ServeHTTP

gin框架剖析(一)

這基本是整個過程的程式碼。

ln, err := net。Listen(“tcp”, addr)做了初試化了socket, bind, listen的操作。

rw, e := l。Accept()進行accept, 等待客戶端進行連線。

go c。serve(ctx) 啟動新的goroutine來處理本次請求。 同時主goroutine繼續等待客戶端連線, 進行高併發操作。

h, _ := mux。Handler(r) 獲取註冊的路由, 然後拿到這個路由的handler, 然後將處理結果返回給客戶端。

從這裡也能夠看出來, net/http基本上提供了全套的服務。

為什麼會出現很多go框架

gin框架剖析(一)

從這段函式可以看出來, 匹配規則過於簡單, 當能匹配到路由的時候就返回其對應的handler, 當不能匹配到時就返回/。 net/http的路由匹配根本就不符合 RESTful 的規則,遇到稍微複雜一點的需求時,這個簡單的路由匹配規則簡直就是噩夢。

所以基本所有的go框架乾的最主要的一件事情就是重寫net/http的route。我們直接說 gin就是一個 httprouter 也不過分, 當然gin也提供了其他比較主要的功能, 後面會一一介紹。

綜述, net/http基本已經提供http服務的70%的功能, 那些號稱賊快的go框架, 基本上都是提供一些功能, 讓我們能夠更好的處理客戶端發來的請求。 如果你有興趣的話,也可以基於 net/http 做一個 Go 框架出來。

gin框架剖析(一)

這個例子中 http。HandleFunc 透過看原始碼,可以看到 URI “/” 被註冊到了 DefaultServeMux 上。

gin框架剖析(一)

net/http ServeHTTP 的作用

net/http 裡面有個非常重要的 Handler interface。只有實現了這個方法才能請求的處理邏輯引入自己的處理流程中。

gin框架剖析(一)

預設的 DefaultServeMux 就實現了這個 ServeHTTP。

這個 request 的流轉過程:

socket。accept 接收到客戶端請求後,啟動 go c。serve(connCtx) [net/http server。go:L3013]行,專門處理這次請求,server 繼續等待客戶端連線。

獲取能處理這次請求的 handler -> serverHandler{c。server}。ServeHTTP(w, w。req) [net/http server。go:L1952]。

跳轉到真正的 ServeHTTP 去匹配路由,獲取 handler。

由於並沒有自定義路由,於是使用的是 net/http 預設路由 [net/http server。go:L2880-2887]。

所以最終呼叫去 DefaultServeMux 匹配路由,輸出返回對應的結果。

gin框架剖析(一)

探究 gin ServeHTTP 的呼叫鏈路

下面是 gin 的官方 demo, 僅僅幾行程式碼,就啟動了一個 echo server:

gin框架剖析(一)

這段程式碼的大概流程:

r := gin。Default() 初始化了相關的引數。

將路由 /ping 以及對應的 handler 註冊到路由樹中。

使用 r。Run() 啟動 server。

r。Run 的底層依然是 http。ListenAndServe。

gin框架剖析(一)

所以 gin 建立 socket 的過程,accept 客戶端請求的過程與 net/http 沒有差別,會同樣重複上面的過程。唯一有差別的位置就是在於獲取 ServeHTTP 的位置

gin框架剖析(一)

由於 sh。srv。Handler 是 interface 型別,但是其真正的型別是 gin。Engine,根據 interace 的動態轉發特性,最終會跳轉到 gin。Engine。ServeHTTP 函式中。

gin框架剖析(一)

gin.ServeHTTP 的實現:

gin框架剖析(一)

至此,終於我們看到了 gin。ServeHTTP 的全貌了

從 sync。pool 裡面拿去一塊記憶體,

對這塊記憶體做初始化工作,防止資料汙染,

處理請求 handleHTTPRequest,

請求處理完成後,把這塊記憶體歸還到 sync。pool 中,

現在看起來這個實現很簡單,其實不然,這才是 gin 能夠處理資料的第一步,也僅僅將請求流轉入 gin 的處理流程而已。

這裡做個結論:

透過上面的原始碼流程分析,我們知道 net/http.ServeHTTP 這個函式相當重要性, 主要有這個函式的存在, 才能將請求流轉入目前 Go 的這些框架裡面

。同學們有興趣的話,可以去看看 echo, iris, go-zero 等框架是如何實現 ServeHTTP 的。

什麼是路由?

這個其實挺容易理解的,就是

根據不同的 URL 找到對應的處理函式

即可。

目前業界 Server 端 API 介面的設計方式一般是遵循 RESTful 風格的規範。當然我也見過某些大公司為了降低開發人員的心智負擔和學習成本,介面完全不區分 GET/POST/DELETE 請求,完全靠介面的命名來表示。

舉個簡單的例子,如:“刪除使用者”

gin框架剖析(一)

這種 No RESTful 的方式,有的時候確實減少一些溝通問題和學習成本,但是隻能內部使用了。這種不區分 GET/POST 的 Web 框架一般設計的會比較靈活,但是開發人員水平參差不齊,會導致出現很多“介面毒瘤”,等你發現的時候已經無可奈何了,如下面這些介面:

gin框架剖析(一)

這樣的介面設計會導致開源的框架都是解析不了的,只能自己手動一層一層 decode 字串,這裡就不再詳細鋪開介紹了,等下一節說到 gin Bind 系列函式時再詳細說一下。

繼續回到上面 RESTful 風格的介面上面來,拿下面這些簡單的請求來說:

gin框架剖析(一)

這是比較規範的 RESTful API設計,分別代表:

獲取 userID 的使用者資訊

更新 userID 的使用者資訊(當然還有其 json body,沒有寫出來)

建立 userID 的使用者(當然還有其 json body,沒有寫出來)

刪除 userID 的使用者

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

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

gin 路由設計

如何設計不同的 Method ?

透過上面的介紹,已經知道

RESTful 是要區分方法的,不同的方法代表意義也完全不一樣

,gin 是如何實現這個的呢?

其實很簡單,不同的方法就是一棵路由樹,所以當 gin 註冊路由的時候,會根據不同的 Method 分別註冊不同的路由樹。

gin框架剖析(一)

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

gin框架剖析(一)

其實程式碼也很容易看懂:

拿到一個 method 方法時,去 trees slice 中遍歷,

如果 trees slice 存在這個 method, 則這個URL對應的 handler 直接新增到找到的路由樹上;

如果沒有找到,則重新建立一顆新的方法樹出來, 然後將 URL對應的 handler 新增到這個路由 樹上。

gin 路由的註冊過程

gin框架剖析(一)

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

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

gin框架剖析(一)

使用 RouteGroup

gin框架剖析(一)

RouteGroup 是非常重要的功能,舉個例子:一個完整的 server 服務,url 需要分為鑑權介面和非鑑權介面,就可以使用 RouteGroup 來實現。其實最常用的,還是用來區分介面的版本升級。這些操作, 最終都會在反應到gin的路由樹上

gin

路由的具體實現

gin框架剖析(一)

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

URL->ping 放在哪裡了。

handler-> 放在哪裡了。

URL 和 handler 是如何關聯起來的。

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

gin框架剖析(一)

在呼叫POST, GET, HEAD等路由HTTP相關函式時, 會呼叫handle函式。handle 是 gin 路由的統一入口。

gin框架剖析(一)

2. 生成路由樹

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

gin框架剖析(一)

當然最簡單最粗暴的就是每個字串佔用一個樹的葉子節點,不過這種設計會帶來的問題:佔用記憶體會升高,我們看到 abc, abd, af 都是用共同的字首的,如果能共用字首的話,是可以省記憶體空間的。

gin 路由樹是一棵字首樹。 我們前面說過 gin 的每種方法(POST, GET 。。。)都有自己的一顆樹,當然這個是根據你註冊路由來的,並不是一上來把每種方式都註冊一遍。

gin 每棵路由大概是下面的樣子:

gin框架剖析(一)

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

3.handler

URL

關聯

gin框架剖析(一)

node 是路由樹的整體結構:

children 就是一顆樹的葉子結點。每個路由的去掉字首後,都被分佈在這些 children 數組裡

path 就是當前葉子節點的最長的字首

handlers 裡面存放的就是當前葉子節點對應的路由的處理函式

當收到客戶端請求時,如何找到對應的路由的handler?

《gin 原始碼閱讀(2) - http請求是如何流入gin的?》第二篇說到 net/http 非常重要的函式 ServeHTTP,當 server 收到請求時,必然會走到這個函數里。由於 gin 實現這個 ServeHTTP,所以流量就轉入 gin 的邏輯裡面。

gin框架剖析(一)

所以,當 gin 收到客戶端的請求時, 第一件事就是去路由樹裡面去匹配對應的 URL,找到相關的路由, 拿到相關的處理函式。其實這個過程就是 handleHTTPRequest 要乾的事情。

gin框架剖析(一)

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

遍歷所有的路由樹,找到對應的方法的那棵樹,

匹配對應的路由,

找到對應的 handler。

總結:說到這裡,基本上把 gin 路由的整個流程說清楚了,本期關於gin就介紹到這,後期會繼續更新。

頂部