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入手:
r。Run() 的原始碼:
看到開始呼叫的是 http。ListenAndServe(address, engine), 這個函式是net/http的函式, 然後請求資料就在net/http開始流轉。
Request 資料是如何流轉的
先不使用gin, 直接使用net/http來處理http請求:
在瀏覽器中輸入localhost:8000, 會看到Hello World。 下面利用這個簡單demo看下request的流轉流程。
HTTP是如何建立起來的
簡單的說一下http請求是如何建立起來的:(需要有基本的網路基礎, 可以找相關的書籍檢視, 推薦看UNIX網路程式設計卷1:套接字聯網API)
TCP/IP 五層模型
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入手檢視原始碼。
註冊路由
這段程式碼是在註冊一個路由及這個路由的handler到DefaultServeMux中。
可以看到這個路由註冊太過簡單了, 也就給gin, iris, echo等框架留下了擴充套件的空間, 後面詳細說這個東西。
服務監聽及響應
上面路由已經註冊到net/http了, 下面就該如何建立socket了, 以及最後又如何取到已經註冊到的路由, 將正確的響應資訊從handler中取出來返回給客戶端。
1.
建立 socket
2.Accept 等待客戶端連結
3.提供回撥介面 ServeHTTP
4.回撥到實際要執行的 ServeHTTP
這基本是整個過程的程式碼。
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框架
從這段函式可以看出來, 匹配規則過於簡單, 當能匹配到路由的時候就返回其對應的handler, 當不能匹配到時就返回/。 net/http的路由匹配根本就不符合 RESTful 的規則,遇到稍微複雜一點的需求時,這個簡單的路由匹配規則簡直就是噩夢。
所以基本所有的go框架乾的最主要的一件事情就是重寫net/http的route。我們直接說 gin就是一個 httprouter 也不過分, 當然gin也提供了其他比較主要的功能, 後面會一一介紹。
綜述, net/http基本已經提供http服務的70%的功能, 那些號稱賊快的go框架, 基本上都是提供一些功能, 讓我們能夠更好的處理客戶端發來的請求。 如果你有興趣的話,也可以基於 net/http 做一個 Go 框架出來。
這個例子中 http。HandleFunc 透過看原始碼,可以看到 URI “/” 被註冊到了 DefaultServeMux 上。
net/http ServeHTTP 的作用
net/http 裡面有個非常重要的 Handler interface。只有實現了這個方法才能請求的處理邏輯引入自己的處理流程中。
預設的 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 ServeHTTP 的呼叫鏈路
下面是 gin 的官方 demo, 僅僅幾行程式碼,就啟動了一個 echo server:
這段程式碼的大概流程:
r := gin。Default() 初始化了相關的引數。
將路由 /ping 以及對應的 handler 註冊到路由樹中。
使用 r。Run() 啟動 server。
r。Run 的底層依然是 http。ListenAndServe。
所以 gin 建立 socket 的過程,accept 客戶端請求的過程與 net/http 沒有差別,會同樣重複上面的過程。唯一有差別的位置就是在於獲取 ServeHTTP 的位置
由於 sh。srv。Handler 是 interface 型別,但是其真正的型別是 gin。Engine,根據 interace 的動態轉發特性,最終會跳轉到 gin。Engine。ServeHTTP 函式中。
gin.ServeHTTP 的實現:
至此,終於我們看到了 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 請求,完全靠介面的命名來表示。
舉個簡單的例子,如:“刪除使用者”
這種 No RESTful 的方式,有的時候確實減少一些溝通問題和學習成本,但是隻能內部使用了。這種不區分 GET/POST 的 Web 框架一般設計的會比較靈活,但是開發人員水平參差不齊,會導致出現很多“介面毒瘤”,等你發現的時候已經無可奈何了,如下面這些介面:
這樣的介面設計會導致開源的框架都是解析不了的,只能自己手動一層一層 decode 字串,這裡就不再詳細鋪開介紹了,等下一節說到 gin Bind 系列函式時再詳細說一下。
繼續回到上面 RESTful 風格的介面上面來,拿下面這些簡單的請求來說:
這是比較規範的 RESTful API設計,分別代表:
獲取 userID 的使用者資訊
更新 userID 的使用者資訊(當然還有其 json body,沒有寫出來)
建立 userID 的使用者(當然還有其 json body,沒有寫出來)
刪除 userID 的使用者
可以看到同樣的 URI,不同的請求 Method,最終其他代表的要處理的事情也完全不一樣。
看到這裡你可以思考一下,假如讓你來設計這個路由,要滿足上面的這些功能,你會如何設計呢?
gin 路由設計
如何設計不同的 Method ?
透過上面的介紹,已經知道
RESTful 是要區分方法的,不同的方法代表意義也完全不一樣
,gin 是如何實現這個的呢?
其實很簡單,不同的方法就是一棵路由樹,所以當 gin 註冊路由的時候,會根據不同的 Method 分別註冊不同的路由樹。
如這四個請求,分別會註冊四顆路由樹出來。
其實程式碼也很容易看懂:
拿到一個 method 方法時,去 trees slice 中遍歷,
如果 trees slice 存在這個 method, 則這個URL對應的 handler 直接新增到找到的路由樹上;
如果沒有找到,則重新建立一顆新的方法樹出來, 然後將 URL對應的 handler 新增到這個路由 樹上。
gin 路由的註冊過程
這段簡單的程式碼裡,r。Get 就註冊了一個路由 /ping 進入 GET tree 中。這是最普通的,也是最常用的註冊方式。
不過上面這種寫法,一般都是用來測試的,正常情況下我們會將 handler 拿到 Controller 層裡面去,註冊路由放在專門的 route 管理裡面,這裡就不再詳細拓展,等後面具體說下 gin 的架構分層設計。
使用 RouteGroup
RouteGroup 是非常重要的功能,舉個例子:一個完整的 server 服務,url 需要分為鑑權介面和非鑑權介面,就可以使用 RouteGroup 來實現。其實最常用的,還是用來區分介面的版本升級。這些操作, 最終都會在反應到gin的路由樹上
gin
路由的具體實現
還是從這個簡單的例子入手。我們只需要弄清楚下面三個問題即可:
URL->ping 放在哪裡了。
handler-> 放在哪裡了。
URL 和 handler 是如何關聯起來的。
1.GET/POST/DELETE/..的最終歸宿
在呼叫POST, GET, HEAD等路由HTTP相關函式時, 會呼叫handle函式。handle 是 gin 路由的統一入口。
2. 生成路由樹
下面考慮一個情況,假設有下面這樣的路由,你會怎麼設計這棵路由樹?
當然最簡單最粗暴的就是每個字串佔用一個樹的葉子節點,不過這種設計會帶來的問題:佔用記憶體會升高,我們看到 abc, abd, af 都是用共同的字首的,如果能共用字首的話,是可以省記憶體空間的。
gin 路由樹是一棵字首樹。 我們前面說過 gin 的每種方法(POST, GET 。。。)都有自己的一顆樹,當然這個是根據你註冊路由來的,並不是一上來把每種方式都註冊一遍。
gin 每棵路由大概是下面的樣子:
這個流程的程式碼太多,這裡就不再貼出具體程式碼裡,有興趣的同學可以按照這個思路看下去即可。
3.handler
與
URL
關聯
node 是路由樹的整體結構:
children 就是一顆樹的葉子結點。每個路由的去掉字首後,都被分佈在這些 children 數組裡
path 就是當前葉子節點的最長的字首
handlers 裡面存放的就是當前葉子節點對應的路由的處理函式
當收到客戶端請求時,如何找到對應的路由的handler?
《gin 原始碼閱讀(2) - http請求是如何流入gin的?》第二篇說到 net/http 非常重要的函式 ServeHTTP,當 server 收到請求時,必然會走到這個函數里。由於 gin 實現這個 ServeHTTP,所以流量就轉入 gin 的邏輯裡面。
所以,當 gin 收到客戶端的請求時, 第一件事就是去路由樹裡面去匹配對應的 URL,找到相關的路由, 拿到相關的處理函式。其實這個過程就是 handleHTTPRequest 要乾的事情。
從程式碼上看這個過程其實也很簡單:
遍歷所有的路由樹,找到對應的方法的那棵樹,
匹配對應的路由,
找到對應的 handler。
總結:說到這裡,基本上把 gin 路由的整個流程說清楚了,本期關於gin就介紹到這,後期會繼續更新。