golang基於context的web範式

golang基於context的web範式

適用框架:golf、echo、gin、dotweb、iris、beego。html

本文章未徹底完成,指定部分框架的主要結構mysql

golang大部分框架都是基於標準庫net/http包現實,fasthttp框架就是本身解析http協議,重新實現了相似net/http包的功能。git

一般框架包含的部分有Application、Context、Request、Response、Router、Middleware、Logger、Binder、render、View、Session、Cache這些部分,通常都是有部分,不過前五個是必定存在。github

如下列出了各框架主要部分定義位置:golang

golf echo gin dotweb iris beego
Application app.go echo.go gin.go dotweb.go iris.go app.go
Context context.go context.go context.go context.go context.go context.go
Request http.Request http.Request http.Request request.go http.Request input.go
Response http.ResponseWriter response.go response_writer_1.8.go response.go response_writer.go output.go
Router router.go router.go routergroup.go router.go router.go router.go
Middleware middleware.go echo.go gin.go middleware.go handler.go app.go
Logger log.go logger.go logger.go log.go
Binder bind.go binding.go bind.go
Render render.go render.go
View view.go engine.go
Session session.go session.go session.go session.go session.go
Cache cache.go cache.go cache.go
Websocket websocket.go server.go
MVC controller.go controller.go

源碼解析:golfweb

Application

application通常都是框架的主體,一般XXX框架的主體叫作XXX,固然也有叫App、Application、Server的實現,具體狀況不你一致,不過通常都是叫XXX,源碼就是XXX.go。redis

這部分通常都會實現兩個方法Start() errorServeHTTP(ResponseWriter, *Request)sql

Start() error通常是框架的啓動函數,用來啓動服務,名稱可能會是Run,建立一個http.Server對象,設置TLS相關等配置,而後啓動服務,固然也會出現Shutdown方法。數據庫

ServeHTTP(ResponseWriter, *Request)函數實現http.Handler接口,通常框架都是使用http.Server對象來啓動的服務,因此須要實現此方法。後端

此本方法大概就三步,Init、Handle、Release。

第一步Init,通常就是初始化Context對象,其中包括Request和Response的初始化Reset,使用ResponseWriter*Request對象來初始化,一般會使用Sync.Pool來回收釋放減小GC。

第二步Handle,一般就是框架處理請求,其中必定包含路由處理,使用路由匹配出對應的Handler來處理當前請求。

第三步釋放Context等對象。

簡單實現:

Router簡單實現

// 定義Application
type Application struct {
	mux		*Router
	pool	sync.Pool
}

func NewApplication() *Application{
	return &Application{
		mux:	new(Router),
		pool:	sync.Pool{
			New:	func() interface{} {
				return &Context{}
			},
		}
	}
}

// 註冊一個GET請求方法,其餘類型
func (app *Application) Get(path string, handle HandleFunc) {
	app.RegisterFunc("GET", path, handle)
}

// 調用路由註冊一個請求
func (app *Application) RegisterFunc(method string, path string, handle HandleFunc)  {
	app.router.RegisterFunc(method, path, handle)
}

// 啓動Application
func (app *Application) Start(addr string) error {
	// 建立一個http.Server並啓動
	return http.Server{
		Addr:		addr,
		Handler:	app,
	}.ListenAndServe()
}

// 實現http.Handler接口,並出去net/http請求。
func (app *Application) ServeHTTP(w http.ResponseWriter, r *http.Request) {
	// 建立一個ctx對象
	ctx := app.pool.Get().(*Context)
	// 初始化ctx
	ctx.Reset(w, r)	
	// 路由器匹配請求並處理ctx
	app.router.Match(r.Method, r.URL.Path)(ctx)
	// 回收ctx
	app.pool.Put(ctx)
}
複製代碼

Context

Context包含Request和Response兩部分,Request部分是請求,Response是響應。Context的各類方法基本都是圍繞Request和Response來實現來,一般就是各類請求信息的獲取和寫入的封裝。

簡單實現:

// Context簡單實現使用結構體,不使用接口,若有其餘須要可繼續增長方法。
type Context struct {
	http.ResponseWriter
	req *http.Request
}

// 初始化ctx
func (ctx *Context) Reset(w http.ResponseWriter, r *http.Request) {
	ctx.ResponseWriter, ctx.req = w, r
}

// Context實現獲取方法
func (ctx *Context) Method() string {
	return ctx.req.Method
}

複製代碼

RequestReader & ResponseWriter

http協議解析文檔

實現RequestReader和ResponseWriter接口。

根據http協議請求報文和響應報文RequestReader和ResponseWriter大概定義以下:

type (
	Header map[string][]string
	RequestReader interface {
		Method() string
		RequestURI() string
		Proto() string
		Header() Header
		Read([]byte) (int, error)
	}
	ResponseWriter interface {
		WriteHeader(int)
		Header() Header
		Write([]byte) (int, error)
	}
)

複製代碼

RequestReader用於讀取http協議請求的請求行(Request Line)、請求頭(Request Header)、body。

ResponseWriter用於返回http寫法響應的狀態行(Statue Line)、響應頭(Response Header)、body這些數據。

在實際過程還會加入net.Conn對象的tcp鏈接信息。

一般net/http庫下的RequestReader和ResponseWriter定義爲http.Request和http.ResponseWriter,請求是一個結構體,擁有請求信息,不一樣狀況下可能會有不一樣封裝,或者直接使用net/http定義的讀寫對象。

Router

Router是請求匹配的路由,並不複雜,可是每一個框架都是必備的。

一般實現兩個方法Match和RegisterFunc,給路由器註冊新路由,匹配一個請求的路由,而後處理請求。

type (
	HandleFunc func(*Context) Router interface{
		Match(string, string) HandleFunc
		RegisterFunc(string, string, HandleFunc)
	}
)
複製代碼

定義一個很是很是簡單的路由器。

type Router struct {
	Routes	map[string]map[string]HandleFunc
}

// 匹配一個Context的請求
func (r *Router) Match(path ,method string) HandleFunc {
	// 查找方法定義的路由
	rs, ok := r.Routes[method]
	if !ok {
		return Handle405
	}
	// 查找路由
	h, ok := rs[path]
	if !ok {
		return Handle404
	}
	return h
}

// 註冊路由處理函數
func (r *Router) RegisterFunc(method string, path string, handle HandleFunc) {
	rs, ok := r.Routes[ctx.Method()]
	if !ok {
		rs = make(map[string]HandleFunc)
		r.Routes[ctx.Method()] = rs
	}
	rs[path] = handle
}

// 處理405響應,方法不容許;Allow Header返回容許的方法。
func Handle405(ctx Context) {
	ctx.Response().WriteHeader(405)
	ctx.Response().Header().Add("Allow", "GET, POST, HEAD")
}


// 處理404響應,沒有找到對應的資源。
func Handle404(ctx Context) {
	ctx.Response().WriteHeader(404)
}
複製代碼

這個簡單路由僅支持了rsetful風格,連通配符匹配都沒有實現;可是體現了路由器的做用,輸出一個參數,返回一個對應的處理者。

至於如何對應一個處理路由,就是路由器規則的設定了;例如通配符、參數、正則、數據校驗等功能。

Middleware

一般是多個Handler函數組合,在handler以前以後增一些處理函數。

echo

type MiddlewareFunc func(HandlerFunc) HandlerFunc // echo.ServeHTTP h := NotFoundHandler for i := len(e.premiddleware) - 1; i >= 0; i-- {
	h = e.premiddleware[i](h)
}
if err := h(c); err != nil {
	e.HTTPErrorHandler(err, c)
}
複製代碼

echo中間件使用裝飾器模式。

echo中間件使用HandlerFunc進行一層層裝飾,最後返回一個HandlerFunc處理Context

gin

gin在路由註冊的會中間件和route合併成一個handlers對象,而後httprouter返回匹配返回handlrs,在context reset時設置ctx的handlers爲路由匹配出現的,handlers是一個HanderFunc數組,Next方法執行下一個索引的HandlerFunc,若是在一個HandlerFunc中使用ctx.Next()就先將後續的HandlerFunc執行,後續執行完纔會繼續那個HandlerFunc,調用ctx.End() 執行索引直接修改成最大值,應該是64以上,畢竟Handlers合併時的數據長度限制是64,執行索引成最大值了,那麼後面就沒有HandlerFunc,就完整了一次ctx的處理。

type HandlerFunc func(*Context) // https://github.com/gin-gonic/gin/blob/master/context.go#L105 func (c *Context) Next() {
	c.index++
	for s := int8(len(c.handlers)); c.index < s; c.index++ {
		c.handlers[c.index](c)
	}
}
複製代碼

echo經過路由匹配返回一個[]HandlerFunc對象,並保存到Context裏面,執行時按ctx.index一個個執行。

若是HandlerFunc裏面調用ctx.Next(),就會提早將後序HandlerFunc執行,返回執行ctx.Next()後的內容,能夠簡單的指定調用順序,ctx.Next()以前的在Handler前執行,ctx.Next()以後的在Handler後執行。

Context.handlers存儲本次請求全部HandlerFunc,而後使用c.index標記固然處理中HandlerFunc,

dotweb

dotweb Middleware接口中Exclude()、HasExclude()、ExistsExcludeRouter()用來排除路由級調用。

BaseMiddlware通常是Middleware的基礎類,一般只重寫Handle()方法,同時能夠調用Next()方法執行寫一個Middleware。

在Next()方法中,若是next爲空就調用Context的RouterNode()方法,得到RouterNode對象,而後使用AppMiddlewares()方法讀取app級別[]Middleware,對其中第一個Middleware調用Handle()就會開始執行app級別Middleware。

同級多個Middleware是單鏈的形式存儲,那麼只有最後一個Middleware的next爲空,若是執行到這個Middleware,那麼表示這一級Middleware所有處理完畢,就須要Context的middleware級別降級,而後開始執行下一級。

ServerHttp()時,調用

//middleware執行優先級:
//優先級1:app級別middleware
//優先級2:group級別middleware
//優先級3:router級別middleware

// Middleware middleware interface
type Middleware interface {
	Handle(ctx Context) error
	SetNext(m Middleware)
	Next(ctx Context) error
	Exclude(routers ...string)
	HasExclude() bool
	ExistsExcludeRouter(router string) bool
}


type RouterNode interface {
    Use(m ...Middleware) *Node
    AppMiddlewares() []Middleware
    GroupMiddlewares() []Middleware
    Middlewares() []Middleware
    Node() *Node
}

// Use registers a middleware
func (app *DotWeb) Use(m ...Middleware) {
	step := len(app.Middlewares) - 1
	for i := range m {
		if m[i] != nil {
			if step >= 0 {
				app.Middlewares[step].SetNext(m[i])
			}
			app.Middlewares = append(app.Middlewares, m[i])
			step++
		}
	}
}


func (bm *BaseMiddlware) Next(ctx Context) error {
	httpCtx := ctx.(*HttpContext)
	if httpCtx.middlewareStep == "" {
		httpCtx.middlewareStep = middleware_App
	}
	if bm.next == nil {
		if httpCtx.middlewareStep == middleware_App {
			httpCtx.middlewareStep = middleware_Group
			if len(httpCtx.RouterNode().GroupMiddlewares()) > 0 {
				return httpCtx.RouterNode().GroupMiddlewares()[0].Handle(ctx)
			}
		}
		if httpCtx.middlewareStep == middleware_Group {
			httpCtx.middlewareStep = middleware_Router
			if len(httpCtx.RouterNode().Middlewares()) > 0 {
				return httpCtx.RouterNode().Middlewares()[0].Handle(ctx)
			}
		}

		if httpCtx.middlewareStep == middleware_Router {
			return httpCtx.Handler()(ctx)
		}
	} else {
		//check exclude config
		if ctx.RouterNode().Node().hasExcludeMiddleware && bm.next.HasExclude() {
			if bm.next.ExistsExcludeRouter(ctx.RouterNode().Node().fullPath) {
				return bm.next.Next(ctx)
			}
		}
		return bm.next.Handle(ctx)
	}
	return nil
}

func (x *xMiddleware) Handle(ctx Context) error {
	httpCtx := ctx.(*HttpContext)
	if httpCtx.middlewareStep == "" {
		httpCtx.middlewareStep = middleware_App
	}
	if x.IsEnd {
		return httpCtx.Handler()(ctx)
	}
	return x.Next(ctx)
}
複製代碼

iris

// If Handler panics, the server (the caller of Handler) assumes that the effect of the panic was isolated to the active request.
// It recovers the panic, logs a stack trace to the server error log, and hangs up the connection.
type Handler func(Context) // Handlers is just a type of slice of []Handler. // // See `Handler` for more. type Handlers []Handler 複製代碼

Logger

框架基礎日誌接口

type Logger interface {
	Debug(...interface{})
	Info(...interface{})
	Warning(...interface{})
	Error(...interface{})
	Fatal(...interface{})
	WithField(key string, value interface{}) Logger
	WithFields(fields Fields) Logger
}
複製代碼

Binder & Render & View

Binder的做用以各類格式方法解析Request請求數據,而後賦值給一個interface{}。

Render的做用和Binder相反,會把數據安裝選擇的格式序列化,而後寫回給Response部分。

View和Render的區別是Render輸入的數據,View是使用對應的模板渲染引擎渲染html頁面。

Session & Cache

seesion是一種服務端數據存儲方案

持久化

Cache持久化就須要把數據存到其餘地方,避免程序關閉時緩存丟失。

存儲介質通常各類DB、file等均可以,實現通常都是下面幾步操做。

一、getid:從請求讀取sessionid。

二、initSession:用sessionid再存儲中取得對應的數據,通常底層就是[]byte

三、newSession:反序列化成一個map[string]interface{}這樣相似結構Session對象。

四、Set and Get:用戶對Session對象各類讀寫操做。

五、Release:將Session對象序列化成[]byte,而後寫會到存儲。

存儲介質

  • 內存:使用簡單,供測試使用,沒法持久化。

  • 文件:存儲簡單。

  • sqlite:和文件差很少,使用sql方式操做。

  • mysql等:數據共享、數據庫持久化。

  • redis:數據共享、協議對緩存支持好。

  • memcache:協議簡單、方法少、效率好。

  • etcd:少許數據緩存,可用性高。

golang session

一組核心的session接口定義。

type (
	Session interface {
		ID() string							// back current sessionID
		Set(key, value interface{}) error	// set session value
		Get(key interface{}) interface{}	// get session value
		Del(key interface{}) error			// delete session value
		Release(w http.ResponseWriter)		// release value, save seesion to store
	}
	type Provider interface {
		SessionRead(sid string) (Session, error)
	}
)
複製代碼

Provider.SessionRead(sid string) (Session, error)用sid來從Provider讀取一個Session返回,sid就是sessionid通常存儲與cookie中,也可使用url參數值,Session對象會更具sid從對應存儲中讀取數據,而後將數據反序列化來初始化Session對象。

Session.Release(w http.ResponseWriter)從名稱上是釋放這個Seesion,可是通常實際做用是將對應Session對象序列化,而後存儲到對應的存儲實現中,若是隻是讀取Session能夠不Release。

簡單的Seession實現可使用一個map,那麼你的操做就是操做這個map。

在初始化Session對象的時候,使用sessionId去存儲裏面取數據,數據不在內存中,大家一般不是map,比較經常使用的是[]byte,例如memcache就是[]byte,[]byte能夠map之間就須要序列化和反序列化了。

在初始化時,從存儲讀取[]map,反序列化成一個map,而後返回給用戶操做;最後釋放Session對象時,就要將map序列化成[]byte,而後再回寫到存儲之中,保存新修改的數據。

Beego.Seesion

源碼github

使用例子:

func login(w http.ResponseWriter, r *http.Request) {
    sess, _ := globalSessions.SessionStart(w, r)
    defer sess.SessionRelease(w)
    username := sess.Get("username")
    if r.Method == "GET" {
        t, _ := template.ParseFiles("login.gtpl")
        t.Execute(w, nil)
    } else {
        sess.Set("username", r.Form["username"])
    }
}
複製代碼

在beego.session中Store就是一個Session。

一、SessionStart定義在session.go#L193,用戶獲取Store對象,在194line獲取sessionid,在200line用sid從存儲讀取sessio.Store對象。

二、memcache存儲在[124line][4]定義SessionRead函數來初始化對象。

三、其中在[sess_memcache.go#L139][5]使用memcache獲取的數據,使用gob編碼反序列化生成了map[interface{}]interface{}類型的值kv,而後144line把kv賦值給了store。

四、在5七、65 [set&get][6]操做的rs.values,就是第三部序列化生成的對象。

五、最後釋放store對象,定義在[94line][7],先把rs.values使用gob編碼序列化成[]byte,而後使用memcache的set方法存儲到memcache裏面了,完成了持久化存儲。

源碼分析總結:

  • session在多線程讀寫是不安全的,數據可能衝突,init和release中間不要有耗時操做,參考其餘思路同樣,暫無解決方案,實現讀寫對存儲壓力大。

  • 對session只讀就不用釋放session,只讀釋放是無效操做,由於set值和原值同樣。

  • beego.session能夠適配一個beego.cache的後端,實現模塊複用,不過也多封裝了一層。

golang cache

實現Get and Set接口的一種實現。

簡單實現

type Cache interface {
	Delete(key interface{})
	Load(key interface{}) (value interface{}, ok bool)
	Store(key, value interface{})
}
複製代碼

這組接口[sync.Map][]簡化出來的,這種簡單的實現了get&set操做,數據存儲於內存中,map類型也能夠直接實現存儲。

複雜實現

封裝各類DB存儲實現接口便可,這樣就能夠實現共享緩存和持久化存儲。

Websocket

協議見文檔

相關文章
相關標籤/搜索