
2024年在線市場平臺(tái)的11大最佳支付解決方案
在接受requst的過程中,核心是router,目的是為了找到path對(duì)應(yīng)的處理函數(shù)handler。router實(shí)現(xiàn)在Multiplexer(復(fù)用器)中,golang中Multiplexer的實(shí)現(xiàn)基于 ServeMux 結(jié)構(gòu)。
注:本文基于go1.16版本分析,注意不同版本存在的差異
main函數(shù)中間兩行代碼分別實(shí)現(xiàn)了路由注冊(cè) 和 啟動(dòng)服務(wù) 功能。其中 啟動(dòng)服務(wù)http.ListenAndServe 的實(shí)現(xiàn)如下:首先創(chuàng)建一個(gè)Server實(shí)例 server,然后調(diào)用server的同名方法ListenAndServe。代碼如下
// http.ListenAndServe
func ListenAndServe(addr string, handler Handler) error {
server := Server{Addr: addr, Handler: handler}
return server.ListenAndServe()
}
type Server struct {
Addr string
Handler Handler
...
}
type Handler interface {
ServeHTTP(ResponseWriter, *Request)
}
在Server結(jié)構(gòu)體中最關(guān)鍵的字段 Handler 是一個(gè)interface。任何結(jié)構(gòu)體,只要實(shí)現(xiàn)了ServeHTTP方法,就可以稱之為Handler對(duì)象。通過后面流程我們可以知道,處理client請(qǐng)求的協(xié)程正是調(diào)用了Server.Handler的ServeHTTP方法去處理業(yè)務(wù)邏輯。因此一種最簡單的實(shí)現(xiàn)http請(qǐng)求調(diào)用的方式是,我們將業(yè)務(wù)處理函數(shù)包裝成Handler對(duì)象直接通過http.ListenAndServe方法的第二個(gè)參數(shù)傳入,代碼如下
func main() {
http.ListenAndServe("127.0.0.0:8000", Hello{})
}
type Hello struct{}
func (*Hello) ServeHTTP(w http.ResponseWriter, r *http.Request) {
fmt.Println("hello world")
}
上面這種實(shí)現(xiàn)方法有個(gè)顯而易見的問題:所有請(qǐng)求都執(zhí)行同一個(gè)handler,沒有辦法根據(jù)不同path去進(jìn)行不同處理。但http框架的核心功能就是能進(jìn)行 路由注冊(cè) 和 路由查找, ServeMux 結(jié)構(gòu)體正是用來解決這個(gè)痛點(diǎn)的。我們關(guān)注到ServeMux有一個(gè)結(jié)構(gòu)為map的m字段,m的key為url,value為muxEntry結(jié)構(gòu),而在muxEntry中定義存儲(chǔ)了具體的url和handler函數(shù)。所有跟業(yè)務(wù)相關(guān)的path和handler映射信息都是通過m存在ServeMux中。
type ServeMux struct {
mu sync.RWMutex
m map[string]muxEntry
es []muxEntry // slice of entries sorted from longest to shortest.
hosts bool // whether any patterns contain hostnames
}
type muxEntry struct {
h Handler
pattern string
}
現(xiàn)在看一下http.HandleFunc方法如何實(shí)現(xiàn)注冊(cè)路由。這里引入ServeMux的實(shí)例DefaultServeMux對(duì)象(在后面的路由查找中也會(huì)用到它),從流程圖不難看出http.HandleFunc方法通過它調(diào)用ServeMux的同名方法HandleFunc,內(nèi)部調(diào)用ServeMux.Handle方法,完成實(shí)際的路由注冊(cè)功能,代碼如下
// http.HandleFunc
func HandleFunc(pattern string, handler func(ResponseWriter, *Request)) {
DefaultServeMux.HandleFunc(pattern, handler)
}
func (mux *ServeMux) HandleFunc(pattern string, handler func(ResponseWriter, *Request)) {
if handler == nil {
panic("http: nil handler")
}
mux.Handle(pattern, HandlerFunc(handler))
}
func (mux *ServeMux) Handle(pattern string, handler Handler) {
mux.mu.Lock()
defer mux.mu.Unlock()
...
e := muxEntry{h: handler, pattern: pattern}
mux.m[pattern] = e
...
}
那么在ServeMux.HandleFunc中為啥要將傳入的handler封裝成HandlerFunc呢?在前文我們知道處理client請(qǐng)求的協(xié)程通過Server.Handler處理業(yè)務(wù)邏輯,而為了能在路由查找之后直接調(diào)用該handler,net包使用了適配器模式 對(duì)帶有func(ResponseWriter, *Request)簽名的處理函數(shù)進(jìn)行統(tǒng)一封裝。從下面的代碼可以看到,HandlerFunc實(shí)現(xiàn)了Handler interface,它的ServeHTTP方法正是執(zhí)行HandlerFunc方法本身。
type HandlerFunc func(ResponseWriter, *Request)
// ServeHTTP calls f(w, r).
func (f HandlerFunc) ServeHTTP(w ResponseWriter, r *Request) {
f(w, r)
}
注冊(cè)好路由之后,我們?cè)撊绾卧诘诙絾?dòng)服務(wù)中實(shí)現(xiàn)路由查找呢?net包中的做法是讓ServerMux實(shí)現(xiàn)Handler interface,在ServerMux.ServerHttp中封裝路由查找和處理函數(shù)的執(zhí)行。這么做是采用了裝飾模式 ,能將ServerMux作為參數(shù)傳入Server.Handler中,從而實(shí)現(xiàn)更簡潔地調(diào)用。從流程圖我們可以看到,在http.ListenAndServe方法中如果不傳入handler,后面Server對(duì)象會(huì)使用DefaultServeMux作為默認(rèn)的Handler,從而通過它調(diào)用ServerMux.ServerHttp方法實(shí)現(xiàn)路由查找并執(zhí)行。代碼如下:
func (mux *ServeMux) ServeHTTP(w ResponseWriter, r *Request) {
...
h, _ := mux.Handler(r)
h.ServeHTTP(w, r)
}
func (mux *ServeMux) Handler(r *Request) (h Handler, pattern string) {
...
return mux.handler(host, r.URL.Path)
}
func (mux *ServeMux) handler(host, path string) (h Handler, pattern string) {
...
if mux.hosts {
h, pattern = mux.match(host + path)
}
if h == nil {
h, pattern = mux.match(path)
}
...
}
func (mux *ServeMux) match(path string) (h Handler, pattern string)
v, ok := mux.m[path]
if ok {
return v.h, v.pattern
}
for _, e := range mux.es {
if strings.HasPrefix(path, e.pattern) {
return e.h, e.pattern
}
}
return nil, ""
}
讓我們看一下在第二步啟動(dòng)服務(wù)中都做了些什么?http.ListenAndServe方法創(chuàng)建了一個(gè)Server對(duì)象,并且調(diào)用Server.ListenAndServe。該方法會(huì)初始化監(jiān)聽地址Addr,同時(shí)調(diào)用Listen方法設(shè)置監(jiān)聽。最后將監(jiān)聽的TCP對(duì)象傳入Server.Serve方法。
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)
}
Serve方法主要職能是創(chuàng)建一個(gè)上下文對(duì)象,然后調(diào)用Listen的Accept方法用來獲取連接數(shù)據(jù)并用newConn方法創(chuàng)建連接對(duì)象。最后起個(gè)goroutine處理連接請(qǐng)求。因?yàn)槊總€(gè)連接都起了一個(gè)協(xié)程,請(qǐng)求的上下文不同,同時(shí)又保證了go的高并發(fā)。
func (srv *Server) Serve(l net.Listener) error {
...
ctx := context.WithValue(baseCtx, ServerContextKey, srv)
for {
rw, err := l.Accept()
...
c := srv.newConn(rw)
c.setState(c.rwc, StateNew, runHooks)
go c.serve(connCtx)
}
}
newConn創(chuàng)建的實(shí)例調(diào)用自己的serve方法,完成后面的邏輯處理。serve方法使用defer定義了函數(shù)退出時(shí)連接關(guān)閉的相關(guān)處理,然后讀取連接的數(shù)據(jù)并處理讀取完畢時(shí)的狀態(tài),其中核心部分是接下來調(diào)用的 serverHandler{c.server}.ServeHTTP(w, w.req) 處理請(qǐng)求。(方法太長,這里就不放源碼了) serverHandler只有Server結(jié)構(gòu)一個(gè)字段,方法中傳入的是在服務(wù)啟動(dòng)時(shí)創(chuàng)建的Server實(shí)例。它調(diào)用自己的ServeHTTP方法,并在該接口方法中做了一個(gè)重要的事情:初始化Handler。如果server對(duì)象沒有指定Handler,則使用DefaultServeMux作為默認(rèn)值,并調(diào)用該Handler的ServeHTTP方法執(zhí)行業(yè)務(wù)邏輯。就像在前文說到的,net包正是將路由查找和業(yè)務(wù)函數(shù)執(zhí)行封裝在ServeMux.ServeHTTP方法中,讓我們可以通過不指定Handler的方式直接使用它。至此一個(gè)client請(qǐng)求的處理就已經(jīng)完成了。
type serverHandler struct {
srv *Server
}
func (sh serverHandler) ServeHTTP(rw ResponseWriter, req *Request) {
handler := sh.srv.Handler
if handler == nil {
handler = DefaultServeMux
}
...
handler.ServeHTTP(rw, req)
}
net包通過http.HandlerFunc將路由和處理函數(shù)進(jìn)行綁定,添加到DefaultServeMux的m字段里;在http.ListenAndServe方法中默認(rèn)通過DefaultServeMux對(duì)象調(diào)用ServeMux.ServeHTTP來實(shí)現(xiàn)路由查找并執(zhí)行handler。同時(shí)net包也暴露給開發(fā)者Handler interface這個(gè)http服務(wù)的核心入口,讓開發(fā)者可以定義一個(gè)結(jié)構(gòu)體實(shí)現(xiàn)interface來替換掉DefaultServeMux,這樣就能在自定義的結(jié)構(gòu)體中實(shí)現(xiàn)更加豐富的功能。開發(fā)者需要做的僅僅是在在服務(wù)啟動(dòng)的時(shí)候?qū)⒆远x結(jié)構(gòu)體作為handler參數(shù)傳入。接下來要介紹的gin框架正是這么實(shí)現(xiàn)的。
雖然我們可以通過net/http實(shí)現(xiàn)一個(gè)簡單的具備路由功能的http服務(wù),但是net/http本身提供的功能比較簡單,不支持用戶以中間件的形式自定義能力。而且該包暴露的函數(shù)簽名參數(shù)是(w http.ResponseWriter, req *http.Request),開發(fā)者解析請(qǐng)求和回寫結(jié)果都不是很方便,因此產(chǎn)生了很多優(yōu)秀的http框架。其中就有我們的主角gin框架,gin框架具體有以下特點(diǎn):
圖中藍(lán)色部分為net/http內(nèi)部邏輯,綠色部分為gin框架實(shí)現(xiàn)邏輯,通過這個(gè)圖可以很好地理解net/http和gin框架的邊界,清楚gin框架在處理流程中所做的事情。從gin框架圖我們發(fā)現(xiàn)實(shí)際上框架核心邏輯就是實(shí)現(xiàn)一個(gè)以Engine結(jié)構(gòu)為核心的路由,用它代替了 net/http 包的 DefaultServeMux并實(shí)現(xiàn)其邏輯。整個(gè)路由注冊(cè)和請(qǐng)求處理的流程在上文中有比較清晰的闡述,所以接下來的重點(diǎn)放在理解gin框架的核心Engine結(jié)構(gòu),其中包括context、router tree、RouterGroup、中間件與請(qǐng)求鏈條等部分。在接下來的介紹中,我們能夠?qū)σ粋€(gè)成熟的http服務(wù)框架所具備的能力以及它們的實(shí)現(xiàn)有個(gè)比較清晰的認(rèn)知,對(duì)之后搭建自己的http服務(wù)框架有個(gè)好的參考。
Engine是gin框架框架的入口,是框架的核心發(fā)動(dòng)機(jī)。我們通過Engine對(duì)象來進(jìn)行服務(wù)路由注冊(cè)和查找、組裝業(yè)務(wù)處理函數(shù)和中間件、進(jìn)行路由組的管理。這幾部分的能力都是通過其核心字段trees和RouteGroup實(shí)現(xiàn)的。
// Default returns an Engine instance with the Logger and Recovery middleware already attached.
func Default() *Engine {
debugPrintWARNINGDefault()
engine := New()
engine.Use(Logger(), Recovery()) // 默認(rèn)加載日志和異常處理兩個(gè)中間件
return engine
}
// New returns a new blank Engine instance without any middleware attached.
func New() *Engine {
debugPrintWARNINGNew()
// 嵌套R(shí)outerGroup,實(shí)現(xiàn)路由相關(guān)的注冊(cè)
engine := Engine{
RouterGroup: RouterGroup{
Handlers: nil,
basePath: "/",
root: true,
},
...
// 路由樹,根據(jù)路徑快速查找handlers,對(duì)于九種http請(qǐng)求方法分別生成單獨(dú)的路由樹
trees: make(methodTrees, 0, 9),
...
}
// RouterGroup嵌套Engine結(jié)構(gòu),能調(diào)用engine的方法
engine.RouterGroup.engine = engine
// 通過對(duì)象池管理Context,減輕GC壓力,提升系統(tǒng)性能
engine.pool.New = func() interface{} {
return engine.allocateContext()
}
return engine
}
// 添加路由
func (engine *Engine) addRoute(method, path string, handlers HandlersChain)
Engine對(duì)象包含一個(gè)addRoute方法用于添加URL請(qǐng)求處理器,它會(huì)將請(qǐng)求對(duì)應(yīng)的路徑和處理器掛接到相應(yīng)的請(qǐng)求樹中。Engine通過RouterTree進(jìn)行路由的注冊(cè)和查找。
在gin框架中,Engine.trees 使用是基于字典樹的httprouter,路由查找效率高且節(jié)省存儲(chǔ)空間。路由規(guī)則被分成了最多9棵前綴樹,每一個(gè)HTTP Method(POST/GET/…)對(duì)應(yīng)一棵前綴樹,樹的節(jié)點(diǎn)按照URL中的/符號(hào)進(jìn)行層級(jí)劃分,URL支持 :name 形式的名字匹配。
之所以這么設(shè)計(jì),在httprouter的README.md中是這么描述的:由于 URL 路徑具有分層結(jié)構(gòu)并且僅使用有限的一組字符,因此會(huì)存在許多公共前綴。這使我們能夠輕松地將路由拆分為更小的部分。此外路由器為每個(gè)請(qǐng)求方法管理一個(gè)單獨(dú)的樹,一方面它比在每個(gè)節(jié)點(diǎn)中保存一個(gè)方法去映射更節(jié)省空間;另一方面它還允許我們?cè)陂_始查找前綴樹之前減少很多路由問題。
type methodTree struct {
method string
root *node
}
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
}
// HandlerFunc defines the handler used by gin middleware as return value.
type HandlerFunc func(*Context)
// HandlersChain defines a HandlerFunc array.
type HandlersChain []HandlerFunc
每個(gè)節(jié)點(diǎn)除了保存按/符號(hào)分割的URL的某段path之外,還會(huì)掛接若干請(qǐng)求處理函數(shù)構(gòu)成一個(gè)請(qǐng)求處理鏈 HandlersChain。每當(dāng)對(duì)請(qǐng)求進(jìn)行路由查找時(shí),在這棵樹上找到的請(qǐng)求URL對(duì)應(yīng)的節(jié)點(diǎn),拿到對(duì)應(yīng)的請(qǐng)求處理鏈進(jìn)行組裝,等待之后的執(zhí)行。
我們經(jīng)常會(huì)遇到類似的場景,需要基于版本或者模塊將相同前綴的路由放在一起,方便使用。而Engine對(duì)象通過RouterGroup對(duì)路由實(shí)現(xiàn)了分組管理,并且支持分組嵌套和對(duì)組設(shè)置中間件。RouterGroup是對(duì)路由樹的包裝,所有的路由規(guī)則最終都是由它來進(jìn)行管理的。Engine結(jié)構(gòu)體繼承了RouterGroup,所以Engine直接具備了RouterGroup所有的路由管理功能。同時(shí)RouterGroup對(duì)象里還會(huì)包含一個(gè)Engine的指針,可以調(diào)用engine的addRoute方法。
type Engine struct {
RouterGroup
...
}
type RouterGroup struct {
Handlers HandlersChain
basePath string
engine *Engine
root bool
}
type IRouter interface {
Use(...HandlerFunc) IRoutes // 注冊(cè)中間件
// Handle、Any等方法調(diào)用handle完成路由注冊(cè)
Handle(string, string, ...HandlerFunc) IRoutes
Any(string, ...HandlerFunc) IRoutes
...
Group(string, ...HandlerFunc) *RouterGroup // 創(chuàng)建一個(gè)新的路由組
}
// 路由注冊(cè)的入口方法
func (group *RouterGroup) handle(httpMethod, relativePath string, handlers HandlersChain) IRoutes {
absolutePath := group.calculateAbsolutePath(relativePath) // 計(jì)算絕對(duì)路徑
handlers = group.combineHandlers(handlers) // 合并業(yè)務(wù)處理函數(shù)和中間件
group.engine.addRoute(httpMethod, absolutePath, handlers) // 向路由樹添加路由
return group.returnObj()
}
// 拼接前綴路徑
func (group *RouterGroup) calculateAbsolutePath(relativePath string) string {
return joinPaths(group.basePath, relativePath)
}
// 合并處理函數(shù)
func (group *RouterGroup) combineHandlers(handlers HandlersChain) HandlersChain {
...
mergedHandlers := make(HandlersChain, finalSize)
copy(mergedHandlers, group.Handlers)
copy(mergedHandlers[len(group.Handlers):], handlers)
return mergedHandlers
}
RouterGroup實(shí)現(xiàn)了IRouter接口,暴露了一系列路由方法,這些方法最終都是通過調(diào)用Engine.addRoute方法將請(qǐng)求處理器掛接到路由樹中。RouterGroup內(nèi)部有一個(gè)前綴路徑字段basePath,它會(huì)調(diào)用calculateAbsolutePath方法將所有的子路徑都加上這個(gè)前綴再放進(jìn)路由樹中。有了這個(gè)前綴路徑,就可以實(shí)現(xiàn)URL分組功能。RouterGroup調(diào)用combineHandlers方法將分組嵌套下所有組維度設(shè)置的中間件和請(qǐng)求處理函數(shù)進(jìn)行組裝成handlers。
在gin框架中插件和業(yè)務(wù)處理函數(shù)形式是一樣的,都是func(*Context),函數(shù)鏈前面的是插件函數(shù),業(yè)務(wù)處理函數(shù)在鏈的最尾端。當(dāng)我們定義路由時(shí),gin框架會(huì)將插件函數(shù)和業(yè)務(wù)處理函數(shù)合并在一起形成鏈條結(jié)構(gòu)HandlersChain。
type Context struct {
...
handlers HandlersChain
index int8
...
}
// 挨個(gè)調(diào)用函數(shù)鏈中的處理函數(shù)
func (c *Context) Next() {
c.index++
for c.index < int8(len(c.handlers)) {
c.handlers[c.index](c)
c.index++
}
}
const abortIndex int8 = math.MaxInt8 >> 1
func (c *Context) Abort() {
c.index = abortIndex
}
gin框架在接收到客戶端請(qǐng)求后,通過路由樹找到相應(yīng)的處理鏈,構(gòu)造一個(gè)Context對(duì)象,再調(diào)用它的Next()方法進(jìn)入請(qǐng)求處理流程。
gin支持Abort()方法中斷請(qǐng)求鏈的執(zhí)行,它的原理是將Context.index調(diào)整到一個(gè)比較大的數(shù)字,這樣Next()方法中的調(diào)用循環(huán)就會(huì)立即結(jié)束。因?yàn)閳?zhí)行Abort()方法之后,需要讓當(dāng)前函數(shù)內(nèi)后面的代碼邏輯繼續(xù)執(zhí)行,所以不能通過panic的方式或者事件等方式中斷執(zhí)行流。如果在插件中顯示調(diào)用Next()方法,那么它就改變了正常的執(zhí)行順序執(zhí)行流,會(huì)嵌套執(zhí)行流,嵌套執(zhí)行流是讓后續(xù)的處理器在前一個(gè)處理器進(jìn)行到一半的時(shí)候執(zhí)行,等后續(xù)處理器完成執(zhí)行后,再回到前一個(gè)處理器繼續(xù)往下執(zhí)行。
gin框架會(huì)為每個(gè)請(qǐng)求分配單獨(dú)的Context,其中包含了請(qǐng)求的參數(shù)、響應(yīng)、engine、handlers等全部上下文信息,并且Context會(huì)貫穿這次請(qǐng)求的所有流程。由于分配給每個(gè)請(qǐng)求Context,當(dāng)百萬并發(fā)到來時(shí),頻繁的創(chuàng)建對(duì)象會(huì)給golang的GC帶來非常大的壓力,因此gin框架就利用sync.Pool將Context對(duì)象復(fù)用起來。
type Context struct {
writermem responseWriter
Request *http.Request // 請(qǐng)求對(duì)象
Writer ResponseWriter // 響應(yīng)對(duì)象
Params Params // URL路徑匹配參數(shù)
handlers HandlersChain // 需要處理的請(qǐng)求鏈
index int8
fullPath string
engine *Engine
params *Params
skippedNodes *[]skippedNode
mu sync.RWMutex
Keys map[string]interface{} // 自定義上下文信息
Errors errorMsgs // 函數(shù)鏈記錄的每個(gè)handler的錯(cuò)誤信息
Accepted []string
queryCache url.Values
formCache url.Values
sameSite http.SameSite
}
Context對(duì)象提供了非常豐富的方法用于獲取當(dāng)前請(qǐng)求的上下文信息,提供了很多內(nèi)置的數(shù)據(jù)綁定和響應(yīng)形式,其中包括JSON、HTML、Protobuf、MsgPack、Yaml等,它會(huì)為每一種形式都單獨(dú)定制一個(gè)渲染器。所有的渲染器最終調(diào)用內(nèi)置的http.ResponseWriter(Context.Writer)將響應(yīng)對(duì)象轉(zhuǎn)換成字節(jié)流寫到socket中。
// ServeHTTP conforms to the http.Handler interface.
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.ServeHTTP方法中,每次響應(yīng)請(qǐng)求都會(huì)先從臨時(shí)對(duì)象池中取一個(gè)context對(duì)象,使用完之后再放回取。需要注意這個(gè)context是從臨時(shí)對(duì)象池中取出后再reset,而不是使用完之后reset。所以這個(gè)context可能會(huì)包含上一次請(qǐng)求的上下文信息,如果上一次請(qǐng)求開啟新的協(xié)程使用context,那么新請(qǐng)求會(huì)reset這個(gè)context。如果需要在新協(xié)程里保留上下文信息,可以通過Context.Copy() copy這個(gè)context進(jìn)行參數(shù)傳遞 。
文章轉(zhuǎn)自微信公眾號(hào)@IEG增長中臺(tái)技術(shù)團(tuán)隊(duì)
2024年在線市場平臺(tái)的11大最佳支付解決方案
完整指南:如何在應(yīng)用程序中集成和使用ChatGPT API
選擇AI API的指南:ChatGPT、Gemini或Claude,哪一個(gè)最適合你?
用ASP.NET Core 2.1 建立規(guī)范的 REST API — 緩存和并發(fā)
企業(yè)工商數(shù)據(jù)API用哪種?
2024年創(chuàng)建社交媒體帖子的最佳圖像工具API
2024年小型企業(yè)的7個(gè)最佳短信應(yīng)用API
用gin寫簡單的crud后端API接口
最新LangChain+GLM4開發(fā)AI應(yīng)用程序系列(一):快速入門篇
對(duì)比大模型API的內(nèi)容創(chuàng)意新穎性、情感共鳴力、商業(yè)轉(zhuǎn)化潛力
一鍵對(duì)比試用API 限時(shí)免費(fèi)