GO http server (III) 組建簡(jiǎn)易 HTTP Server 框架

上篇提到 DefaultServerMux 作為默認(rèn)的 HTTP Server 框架太過(guò)簡(jiǎn)單峻堰,缺少很多功能咽袜。這篇我們利用官方庫(kù)和一些三方庫(kù)來(lái)定制一個(gè)簡(jiǎn)易合用的 HTTP Server 框架丸卷。完整代碼見(jiàn)這里

Router

首先要有 router 模塊,這里我使用第三方 gorilla 框架的最小化路由模塊 mux询刹,它的作用和 DefaultServerMux 差不多谜嫉,只不過(guò)支持了 RESTful API萎坷。

在添加路由和對(duì)應(yīng) handler 時(shí),很可能我們寫(xiě)的處理函數(shù)有 bug沐兰,導(dǎo)致沒(méi)有往 response 里寫(xiě)入內(nèi)容就返回哆档,這會(huì)造成客戶(hù)端阻塞等待,所以當(dāng)出現(xiàn)錯(cuò)誤提前返回時(shí)僧鲁,需要一個(gè)默認(rèn)的錯(cuò)誤處理函數(shù)虐呻,給客戶(hù)端返回默認(rèn)錯(cuò)誤信息。

import (
    "net/http"

    "github.com/gorilla/mux"
)
type Router struct {
   router     *mux.Router
   ctxPool    sync.Pool
   errHandler func(w http.responseWriter, r *http.request) 
}

很多時(shí)候寞秃,執(zhí)行路由對(duì)應(yīng) handler 時(shí)我們并不想直接操作 http.responseWriter 和 *http.request斟叼,并且希望有一些簡(jiǎn)單的封裝,提供更多的功能春寿。再者朗涩,這兩個(gè)對(duì)象并不能很好的攜帶中間件處理過(guò)程中產(chǎn)生的一些參數(shù)。所以我們會(huì)定義一個(gè) Context (下一節(jié))來(lái)封裝它們绑改。每一個(gè)請(qǐng)求都應(yīng)該有一個(gè) Context谢床,為了方便的管理,使用 sync.Pool 做一個(gè) context 池厘线。

創(chuàng)建新的 Router:

// NewRouter returns a router.
func NewRouter() *Router {
   r := &Router{
      router:     mux.NewRouter(),
      errHandler: func(_ *Context) {},
   }

   r.ctxPool.New = func() interface{} {
      return NewContext(nil, nil)
   }

   r.router.NotFoundHandler = http.NotFoundHandler()
   r.router.MethodNotAllowedHandler = MethodNotAllowedHandler()

   return r
}

router 注冊(cè)路由识腿,由于使用 gorilla.mux,調(diào)用其 HandleFunc 造壮,返回 router 本身渡讼,在調(diào)用 Method 即可指定請(qǐng)求方法。不過(guò)我們還可以在自己的 handler 執(zhí)行之前耳璧,提供一些鉤子成箫,這里我們可以添加一些 filter 函數(shù),以便功能擴(kuò)展旨枯。

type FilterFunc func(*Context) bool

func (rt *Router) Get(pattern string, handler HandlerFunc, filters ...FilterFunc) {
   rt.router.HandleFunc(pattern, rt.wrapHandlerFunc(handler, filters...)).Methods("GET")
}

// Post adds a route path access via POST method.
func (rt *Router) Post(pattern string, handler HandlerFunc, filters ...FilterFunc) {
   rt.router.HandleFunc(pattern, rt.wrapHandlerFunc(handler, filters...)).Methods("POST")
}

// Wraps a HandlerFunc to a http.HandlerFunc.
func (rt *Router) wrapHandlerFunc(f HandlerFunc, filters ...FilterFunc) http.HandlerFunc {
   return func(w http.ResponseWriter, r *http.Request) {
      c := rt.ctxPool.Get().(*Context)
      defer rt.ctxPool.Put(c)
      c.Reset(w, r)

      if len(filters) > 0 {
         for _, filter := range filters {
            if passed := filter(c); !passed {
               c.LastError = errFilterNotPassed
               return
            }
         }
      }

      if err := f(c); err != nil {
         c.LastError = err
         rt.errHandler(c)
      }
   }
}

Context

前面提到可以用一個(gè) Context 包裝 http.responseWriter 和 *http.request蹬昌,并且提供一些額外的功能。額外的功能如 validator攀隔,用來(lái)對(duì)請(qǐng)求做參數(shù)驗(yàn)證皂贩。這個(gè) validator 我們可以直接用一個(gè)第三方庫(kù),也可以做成 Interface 以便升級(jí)昆汹。

另外我們可能需要 Context 能夠攜帶額外的信息先紫,所以可以加一個(gè) map 用來(lái)存儲(chǔ)。

type Context struct {
   responseWriter http.ResponseWriter
   request        *http.Request
   Validator      *validator.Validate
   store          map[string]interface{}
}

不要忘了在 Router 里面我們是用一個(gè)線程安全的池來(lái)管理 context 筹煮,也就是每次用完 context 需要還回去來(lái)避免臨時(shí)分配帶來(lái)的開(kāi)銷(xiāo)。所以別忘了還回去之前需要把 context 重置成原來(lái)的樣子居夹。

func (c *Context) Reset(w http.ResponseWriter, r *http.Request) {
   c.responseWriter = w
   c.request = r
   c.store = make(map[string]interface{})
}

Server

有了 router 和 context败潦,我們還需要封裝一個(gè) server本冲。首先定義一個(gè) EntryPoiont 結(jié)構(gòu)體,當(dāng)然名字隨意劫扒。非常確認(rèn)的是我們需要用到 http 包的 Server檬洞,還可以加上可能用到的 net.Listener。另外沟饥,我們需要方便的添加一些即插即用的工具添怔,所以需要中間件,這里我使用第三方庫(kù) negroni 贤旷。然后我們可能需要一個(gè)通知關(guān)閉所有連接的機(jī)制广料,用一個(gè) channel 可以做到。所以 EntryPoint 大致如下:

type Entrypoint struct {
   server        *http.Server
   listener      net.Listener
   middlewares   []negroni.Handler
}

negroni

其實(shí) negroni 的核心代碼也很簡(jiǎn)單幼驶,就只是把多個(gè) middleware 串起來(lái)使其能夠串行調(diào)用艾杏。

type Negroni struct {
   middleware middleware
   handlers   []Handler
}

type middleware struct {
    handler Handler
    next    *middleware
}

type Handler interface {
    ServeHTTP(rw http.ResponseWriter, r *http.Request, next http.HandlerFunc)
}

關(guān)鍵就是 Handler 接口,所有第三方實(shí)現(xiàn)的中間件要和 negroni 一起用的話盅藻,都要實(shí)現(xiàn)它购桑,并且每個(gè)中間件執(zhí)行完自己的功能后,要去調(diào)用 next 觸發(fā)下一個(gè)中間件的執(zhí)行氏淑。

添加中間件:

func (n *Negroni) Use(handler Handler) {
   if handler == nil {
      panic("handler cannot be nil")
   }

   n.handlers = append(n.handlers, handler)
   n.middleware = build(n.handlers)
}

func build(handlers []Handler) middleware {
    var next middleware

    if len(handlers) == 0 {
        return voidMiddleware()
    } else if len(handlers) > 1 {
        next = build(handlers[1:])
    } else {
        next = voidMiddleware()
    }

    return middleware{handlers[0], &next}
}

添加中間件的時(shí)候勃蜘,遞歸地調(diào)用 build ,把所有 middlewares 串起來(lái)假残。必然的缭贡,negroni 實(shí)現(xiàn)了 http.Handler 接口,這使得 Negroni 可以當(dāng)做 http.Handler 傳給 Server.Serve()

func (n *Negroni) ServeHTTP(rw http.ResponseWriter, r *http.Request) {
   n.middleware.ServeHTTP(NewResponseWriter(rw), r)
}

func (m middleware) ServeHTTP(rw http.ResponseWriter, r *http.Request) {
    m.handler.ServeHTTP(rw, r, m.next.ServeHTTP)
}

整合 router

當(dāng)所有中間件執(zhí)行完了以后守问,應(yīng)該把 context 傳給 router 去執(zhí)行對(duì)應(yīng)的路由匀归,所以把 router 作為最后一個(gè)中間件傳到 negroni 。

func (ep *Entrypoint) buildRouter(router http.Handler) http.Handler {
    n := negroni.New()

    for _, mw := range ep.middlewares {
        n.Use(mw)
    }

    n.Use(negroni.Wrap(http.HandlerFunc(router.ServeHTTP)))

    return n
}

當(dāng)然在啟動(dòng) Server.Serve() 之前耗帕,還要把 ep.buildRouter 返回的對(duì)象賦給 ep.Server.Handler穆端,使這個(gè)對(duì)象代替 DefaultServerMux。

func (ep *Entrypoint) prepare(router http.Handler) error {
   var (
      err       error
      listener  net.Listener
   )

   listener, err = net.Listen("tcp", ep.configuration.Address)
   if err != nil {
      return err
   }

   ep.listener = listener
   ep.server = &http.Server{
      Addr:      ep.configuration.Address,
      Handler:   ep.buildRouter(router),
   }

   return nil
}

接下來(lái)就可以調(diào)用 start 跑起服務(wù):

func (ep *Entrypoint) Start(router http.Handler) error {
   if router == nil {
      return errNoRouter
   }

   if err := ep.prepare(router); err != nil {
      return err
   }

   go ep.startServer()

   fmt.Println("Serving on:", ep.configuration.Address)

   return nil
}

中間件封裝

有的時(shí)候有一些現(xiàn)成的中間件仿便,但是不能直接放到 negroni 里面用体啰,就需要我們給它加一層封裝。

例如嗽仪,我們要做 jwt 驗(yàn)證荒勇,使用第三方的 *jwtmiddleware.JWTMiddleware,但是有的路徑我們不需要 token闻坚,需要跳過(guò) jwt 中間件沽翔。不方便改別人的代碼,可以這樣封裝來(lái)代替原來(lái)的 *jwtmiddleware.JWTMiddleware:

type Skipper func(path string) bool

// JWTMiddleware is a wrapper of go-jwt-middleware, but added a skipper func on it.
type JWTMiddleware struct {
   *jwtmiddleware.JWTMiddleware
   skipper Skipper
}

使用 *jwtmiddleware.JWTMiddleware 作為一個(gè)匿名變量,這樣可以在自定義的 JWTMiddleware 上直接調(diào)用 *jwtmiddleware.JWTMiddleware 的函數(shù)仅偎。然后用 handler 函數(shù)覆蓋原有的 HandlerWithNext 函數(shù)跨蟹,這樣就能通過(guò)調(diào)用時(shí)傳入的 skipper 函數(shù)判斷是否需要跳過(guò) jwt:

func (jm *JWTMiddleware) handler(w http.ResponseWriter, r *http.Request, next http.HandlerFunc) {
   path := r.URL.Path
   if skip := jm.skipper(path); skip {
      next(w, r)
      return
   }

   jm.HandlerWithNext(w, r, next)
}

最后用 negroni 包裝一下,使它能夠直接被 negroni 使用:

func NegroniJwtHandler(key string, skipper Skipper, signMethod *jwt.SigningMethodHMAC, errHandler func(w http.ResponseWriter, r *http.Request, err string)) negroni.Handler {
   if signMethod == nil {
      signMethod = jwt.SigningMethodHS256
   }
   jm := jwtmiddleware.New(jwtmiddleware.Options{
      ValidationKeyGetter: func(token *jwt.Token) (interface{}, error) {
         return []byte(key), nil
      },
      SigningMethod: signMethod,
      ErrorHandler:  errHandler,
   })

   if skipper == nil {
      skipper = defaulSkiper
   }

   JM := JWTMiddleware{
      jm,
      skipper,
   }

   return negroni.HandlerFunc(JM.handler)
}

總結(jié)

目前為止我們實(shí)現(xiàn)了一個(gè)簡(jiǎn)易通用的 HTTP server 框架橘沥,雖然功能還不是很完善窗轩,不過(guò)好在可擴(kuò)展性比較高,我們可以在此基礎(chǔ)上任意擴(kuò)展座咆,可以添加上緩存痢艺、數(shù)據(jù)庫(kù)、監(jiān)控等等模塊介陶。

如果有興趣的話堤舒,可以去看看 echo 的實(shí)現(xiàn),其實(shí)也是大同小異斤蔓。

最后植酥,再放一遍項(xiàng)目地址,還有一些別的庫(kù)弦牡,歡迎 star 和 pr 啦友驮!

?著作權(quán)歸作者所有,轉(zhuǎn)載或內(nèi)容合作請(qǐng)聯(lián)系作者
  • 序言:七十年代末攒射,一起剝皮案震驚了整個(gè)濱河市赡麦,隨后出現(xiàn)的幾起案子宵凌,更是在濱河造成了極大的恐慌长豁,老刑警劉巖咪啡,帶你破解...
    沈念sama閱讀 212,816評(píng)論 6 492
  • 序言:濱河連續(xù)發(fā)生了三起死亡事件捌朴,死亡現(xiàn)場(chǎng)離奇詭異蚓土,居然都是意外死亡厦取,警方通過(guò)查閱死者的電腦和手機(jī)赏酥,發(fā)現(xiàn)死者居然都...
    沈念sama閱讀 90,729評(píng)論 3 385
  • 文/潘曉璐 我一進(jìn)店門(mén)喳整,熙熙樓的掌柜王于貴愁眉苦臉地迎上來(lái),“玉大人裸扶,你說(shuō)我怎么就攤上這事框都。” “怎么了呵晨?”我有些...
    開(kāi)封第一講書(shū)人閱讀 158,300評(píng)論 0 348
  • 文/不壞的土叔 我叫張陵魏保,是天一觀的道長(zhǎng)。 經(jīng)常有香客問(wèn)我摸屠,道長(zhǎng)谓罗,這世上最難降的妖魔是什么? 我笑而不...
    開(kāi)封第一講書(shū)人閱讀 56,780評(píng)論 1 285
  • 正文 為了忘掉前任季二,我火速辦了婚禮檩咱,結(jié)果婚禮上,老公的妹妹穿的比我還像新娘。我一直安慰自己税手,他們只是感情好蜂筹,可當(dāng)我...
    茶點(diǎn)故事閱讀 65,890評(píng)論 6 385
  • 文/花漫 我一把揭開(kāi)白布。 她就那樣靜靜地躺著芦倒,像睡著了一般。 火紅的嫁衣襯著肌膚如雪不翩。 梳的紋絲不亂的頭發(fā)上兵扬,一...
    開(kāi)封第一講書(shū)人閱讀 50,084評(píng)論 1 291
  • 那天,我揣著相機(jī)與錄音口蝠,去河邊找鬼器钟。 笑死,一個(gè)胖子當(dāng)著我的面吹牛妙蔗,可吹牛的內(nèi)容都是我干的傲霸。 我是一名探鬼主播,決...
    沈念sama閱讀 39,151評(píng)論 3 410
  • 文/蒼蘭香墨 我猛地睜開(kāi)眼眉反,長(zhǎng)吁一口氣:“原來(lái)是場(chǎng)噩夢(mèng)啊……” “哼昙啄!你這毒婦竟也來(lái)了?” 一聲冷哼從身側(cè)響起寸五,我...
    開(kāi)封第一講書(shū)人閱讀 37,912評(píng)論 0 268
  • 序言:老撾萬(wàn)榮一對(duì)情侶失蹤梳凛,失蹤者是張志新(化名)和其女友劉穎,沒(méi)想到半個(gè)月后梳杏,有當(dāng)?shù)厝嗽跇?shù)林里發(fā)現(xiàn)了一具尸體韧拒,經(jīng)...
    沈念sama閱讀 44,355評(píng)論 1 303
  • 正文 獨(dú)居荒郊野嶺守林人離奇死亡,尸身上長(zhǎng)有42處帶血的膿包…… 初始之章·張勛 以下內(nèi)容為張勛視角 年9月15日...
    茶點(diǎn)故事閱讀 36,666評(píng)論 2 327
  • 正文 我和宋清朗相戀三年十性,在試婚紗的時(shí)候發(fā)現(xiàn)自己被綠了叛溢。 大學(xué)時(shí)的朋友給我發(fā)了我未婚夫和他白月光在一起吃飯的照片。...
    茶點(diǎn)故事閱讀 38,809評(píng)論 1 341
  • 序言:一個(gè)原本活蹦亂跳的男人離奇死亡劲适,死狀恐怖楷掉,靈堂內(nèi)的尸體忽然破棺而出,到底是詐尸還是另有隱情减响,我是刑警寧澤靖诗,帶...
    沈念sama閱讀 34,504評(píng)論 4 334
  • 正文 年R本政府宣布,位于F島的核電站支示,受9級(jí)特大地震影響刊橘,放射性物質(zhì)發(fā)生泄漏。R本人自食惡果不足惜颂鸿,卻給世界環(huán)境...
    茶點(diǎn)故事閱讀 40,150評(píng)論 3 317
  • 文/蒙蒙 一促绵、第九天 我趴在偏房一處隱蔽的房頂上張望。 院中可真熱鬧,春花似錦败晴、人聲如沸浓冒。這莊子的主人今日做“春日...
    開(kāi)封第一講書(shū)人閱讀 30,882評(píng)論 0 21
  • 文/蒼蘭香墨 我抬頭看了看天上的太陽(yáng)稳懒。三九已至,卻和暖如春慢味,著一層夾襖步出監(jiān)牢的瞬間场梆,已是汗流浹背。 一陣腳步聲響...
    開(kāi)封第一講書(shū)人閱讀 32,121評(píng)論 1 267
  • 我被黑心中介騙來(lái)泰國(guó)打工纯路, 沒(méi)想到剛下飛機(jī)就差點(diǎn)兒被人妖公主榨干…… 1. 我叫王不留或油,地道東北人。 一個(gè)月前我還...
    沈念sama閱讀 46,628評(píng)論 2 362
  • 正文 我出身青樓驰唬,卻偏偏與公主長(zhǎng)得像顶岸,于是被迫代替她去往敵國(guó)和親。 傳聞我的和親對(duì)象是個(gè)殘疾皇子叫编,可洞房花燭夜當(dāng)晚...
    茶點(diǎn)故事閱讀 43,724評(píng)論 2 351

推薦閱讀更多精彩內(nèi)容

  • Spring Cloud為開(kāi)發(fā)人員提供了快速構(gòu)建分布式系統(tǒng)中一些常見(jiàn)模式的工具(例如配置管理辖佣,服務(wù)發(fā)現(xiàn),斷路器宵溅,智...
    卡卡羅2017閱讀 134,638評(píng)論 18 139
  • Android 自定義View的各種姿勢(shì)1 Activity的顯示之ViewRootImpl詳解 Activity...
    passiontim閱讀 171,870評(píng)論 25 707
  • 批處理符號(hào)簡(jiǎn)介 回顯屏蔽 @ 重定向1 >與>> 重定向2 < 管道符號(hào) | 轉(zhuǎn)義符 ^ 邏輯命令符包括:&凌简、&&...
    wyude閱讀 2,969評(píng)論 2 5
  • 人活著有意義嗎?很多人會(huì)想:“怎么會(huì)沒(méi)有意義呢恃逻,要是沒(méi)有生命多枯燥啊雏搂,肯定有!” 事實(shí)上:“沒(méi)有?芩稹M怪!!" 正因?yàn)闆](méi)...
    關(guān)中人閱讀 696評(píng)論 0 1
  • 路在腳下矛市,夢(mèng)在心中芙沥,心在遠(yuǎn)方。 你可能時(shí)常問(wèn)自己浊吏,人生最遠(yuǎn)的距離會(huì)不會(huì)是遙不可及的天際而昨,而我總是說(shuō)...
    艾薇兒_0c3d閱讀 222評(píng)論 0 3