golang iris mvc框架的服務端載入過程
整個iris框架共三層結構:
- 應用的配置和註冊資訊,如路由、中介軟體、日誌。
- 中間的服務端例項,從iris例項拿配置資訊進行配置。
- 底層net/http包,負責TCP連線建立、監聽接受,請求收取並解析,緩衝區管理,寫入響應。
監聽服務的閉包構建
iris框架包裝了Server為更完善的Supervisor類,資料結構如下:
type Supervisor struct { Server*http.Server closedManually int32 manuallyTLSboolserver begin. shouldWaitint32 unblockChanchan struct{} mu sync.Mutex onServe []func(TaskHost) IgnoredErrors []string onErr[]func(error) onShutdown[]func() }
呼叫NewHost(srv)他建Supervisor例項。先初始化以下欄位:
srv.Addr srv.Handler //app.Router su.unblockChan //chan struct{}
關閉ShutdownGracefully
RegisterOnServe,註冊一個向日志列印閉包。按下CMD+C中斷的閉包。全域性中斷註冊地:
// onInterrupt contains a list of the functions that should be called when CTRL+C/CMD+C or a unix kill //command received. var Interrupt = new(interruptListener) type interruptListener struct { musync.Mutex once sync.Once onInterrupt []func() }
具體實現的閉包是RegisterOnInterrupt:
先設定shutdownTimeout超時 su.closedManually++ //要用atomic包原子操作 su.onShutdown[:]() //執行所有的閉包 su.Server.Shutdown(ctx)//net/http標準庫方法 //RestoreFlow su.shouldWait=0 //atomic su.unblockChan <- //mutex //defer ctx.cancel()
優雅關閉流程:
- 先關閉open listeners
- 關閉idle connections
- 等待連線至idle再關閉
- 忽略hijacked connections。不主動關閉,僅通知其關閉
Deferflow和shoudlWait標誌位
func (su *Supervisor) DeferFlow() { atomic.StoreInt32(&su.shouldWait, 1) } func (su *Supervisor) isWaiting() bool { return atomic.LoadInt32(&su.shouldWait) != 0 }
配合unblockChan實現手動關閉su例項的功能。
if su.isWaiting() { blockStatement: for { select { case <-su.unblockChan: break blockStatement } } }
使用樣例:
su.DeferFlow() //不關閉直到主動呼叫 su.Shutdown(ctx) //關閉 su.RestoreFlow() ctx.cancel()
su.closedManually==1表示主動呼叫過su.Shutdown()方法。
匯入app.config.IgnoreServerErrors。
由app載入配置,iris並沒有使用一個欄位的struct。而是使用了閉包的寫法。直接修改Supervisor例項,改變配置,減少資料傳遞提高效率。
[]Configurator func(su *Supervisor)
共有兩次載入配置,一次是NewHost內部,呼叫su.Configure(app.hostConfigurators...),第二次外部呼叫su.Configure(hostConfigurators...)。
最後把Supervisor例項註冊到app.Hosts。
ListenAndServe建立監聽和服務
建立監聽邏輯:
func (su *Supervisor) newListener() (net.Listener, error) { l, err := netutil.TCPKeepAlive(su.Server.Addr) if err != nil { return nil, err } if netutil.IsTLS(su.Server) { // means tls tlsl := tls.NewListener(l, su.Server.TLSConfig) return tlsl, nil } return l, nil } func TCPKeepAlive(addr string) (ln net.Listener, err error) { ln, err = TCP(addr) if err != nil { return nil, err } return tcpKeepAliveListener{ln.(*net.TCPListener)}, nil } func TCP(addr string) (net.Listener, error) { l, err := net.Listen("tcp", addr) if err != nil { return nil, err } return l, nil }
iris框架把net包封裝成了更易用的netutil包。方便建立keepAlive TLS letsencrypt.org 的監聽。API如下:
(l tcpKeepAliveListener) Accept() (c net.Conn, err error) TCP(addr string) (net.Listener, error) TCPKeepAlive(addr string) (ln net.Listener, err error) TLS(addr, certFile, keyFile string) (net.Listener, error) CERT(addr string, cert tls.Certificate) (net.Listener, error) UNIX(socketFile string, mode os.FileMode) (net.Listener, error) LETSENCRYPT(addr string, serverName string, cacheDirOptional ...string) (net.Listener, error)
由netutil包建立的TCP常連線keepalive時間為3分鐘。
呼叫su.Serve(l)方法即呼叫net/http標準庫中的su.Server.Serve(l)。
框架將ListenAndServe及supervisor構建包裝成了閉包。由於閉包內的程式碼需要iris例項和supervisor例項均已構建完成之後執行,所以封裝成閉包,等前iris物件例項化之後再呼叫。
構建iris例項並執行服務
前文說明
前幾篇文章已詳述過,在iris服務端執行之前。iris例項已經載入了handler和middleware。建立好了ctx的物件池,這裡的物件池,是iris自定義的例項,包裹了http請求、響應以及其它連線引數,不同於TCP連線時的KV引數,ctx cancelCtx timerCtx等。日誌載入和配置資訊載入,將在呼叫Runner閉包時執行。
實際iris框架,在Runner閉包呼叫前後,共對iris服務端例項進行了三次配置:
- 註冊階段的配置
- Run()階段呼叫app.Build,功能:builds the default router and the template function。
- Runner閉包內載入func Configurator,功能是配置*Server例項及其包裹例項。仍由少量資訊反饋給iris例項。如包裹*Server的Supervisor例項本身,就會被註冊到app.Host欄位。(可能多例項,要考慮到併發)
在呼叫app.Build()之前,app.APIBuilder欄位已構建完成,包括了路由資訊。ApiBuider是一個路由構建器,提供了路由構建的介面:
type RoutesProvider interface { GetRoutes() []*Route GetRoute(routeName string) *Route }
路由按路徑及其子路徑。路由的儲存結構如下:
type routerHandler struct { trees []*tree hosts bool // true if at least one route contains a Subdomain. } type tree struct { Method string Subdomain string Nodes*node.Nodes } type node struct { sstring routeNamestring wildcardParamName string paramNames[]string childrenNodesNodes handlerscontext.Handlers rootbool rootWildcardbool }
每一段路由路徑,都包含了一系列context.Handlers。路由陣列,只要執行第一向就會向後鏈式執行,幷包裹子節點的路由。
經過以上覆雜的準備,可以執行構建路由了。
最後處理app.view。routeProvider.Path()方法功能是由路由名解析出路由路徑,其中引數用%v代替。
構建routeHandler
先對路由排序,排序規則是:subDomain長的在前。如果子域長度相等且,則第一段路徑在前。如果仍相等,則請求路徑中引數較多的在前。
路由中handler的排序為:beginHandlers Handlers doneHandlers。
接下來完成將路由route新增到routerHandler。路由是用樹的陣列儲存的,每個樹是同一類請求方法,同一個子域,節點是樹狀圖。每個節點包含了:一段路徑 引數名字列表 handlers 子節點。
再看路由的資料結構:
type Route struct { Namestring Methodstring methodBckp string Subdomainstring tmpl*macro.Template beginHandlers context.Handlers Handlerscontext.Handlers MainHandlerName string doneHandlers context.Handlers Pathstring FormattedPath string `json:"formattedPath"` }
先由method, subdomain找到所在tree,再根據path, handler新增路由樹中。
為什麼要三次構建路由
這裡區別APIBuilder和routerHandler儲存路的方式。以及為什麼要三次構建路由。
在mvc controller中:http請求方式 http請求路徑 subdomain(就是controller類的RequestMapping) 一組handlers,共同構成了一條路由路徑。在註冊controller時,以列表的方式加入到APIBuilder,是工作量最小的儲存方式。
但這樣有問題,在服務端拿到httpRequest時,查詢路由的效率極低,需要遍歷整個個數組。因而需要將路由表按subdomain+path構成樹狀結構,加快查詢效率。
整個過程,由控制器新增線性的路由表,並轉成樹狀路由表是在服務端接受http請求之前完成,不會影響http處理效率。接受請求之後,遍歷樹進行查詢,命中則重新構建一條路徑,處理http請求。
樹狀路由新增節點
執行方法:
t.Nodes.Add(routeName, path, handlers)
收尾工作
路由自帶一個context的sync.pool,複用物件加快httpRequest的構建。
保留路由構建工具APIBuilder作為一個欄位。
最後作為Server.Handler型別,要實現ServeHTTP介面。
router.mainHandler = func(w http.ResponseWriter, r *http.Request) { ctx := cPool.Acquire(w, r) router.requestHandler.HandleRequest(ctx) cPool.Release(ctx) } if router.wrapperFunc != nil { router.mainHandler = NewWrapper(router.wrapperFunc, router.mainHandler).ServeHTTP }
需要說明的是router本身也實現了Hanler介面,可以呼叫router.ServeHTTP()。但是這樣使用不會複用contex物件,會帶來效能損耗 。
將符合net/http包Server.Handler介面的實現,內部交由requestHandler.HandleRequest實現。Router是對requestHandler的包裝:
type Router struct { mu sync.Mutex requestHandler RequestHandler mainHandlerhttp.HandlerFunc wrapperFuncfunc(http.ResponseWriter, *http.Request, http.HandlerFunc) cPool*context.Pool routesProvider RoutesProvider }
如果使用了包裹,先執行包裹程式碼。包裹哪些程式碼,被包裹的程式碼段,可以自由配置,利益於閉包的使用。將兩個閉包打包成一個struct{},再實現一個類方法,造知這兩個閉包的程式碼以何種順序執行。如果沒有閉包,函式是寫死在包裹中的,不能更換,進而會增加許多重複的程式碼。
type wrapper struct { routerhttp.HandlerFunc wrapperFunc func(http.ResponseWriter, *http.Request, http.HandlerFunc) } func (wr *wrapper) ServeHTTP(w http.ResponseWriter, r *http.Request) { wr.wrapperFunc(w, r, wr.router) }
更進一步,原始碼中的WrapRouter方法實現了兩層包裹。