GO http server (III) 组建简易 HTTP Server 框架

上篇提到 DefaultServerMux 作为默认的 HTTP Server 框架太过简单,缺少很多功能。这篇我们利用官方库和一些三方库来定制一个简易合用的 HTTP Server 框架。完整代码见这里

Router

首先要有 router 模块,这里我使用第三方 gorilla 框架的最小化路由模块 mux,它的作用和 DefaultServerMux 差不多,只不过支持了 RESTful API。

在添加路由和对应 handler 时,很可能我们写的处理函数有 bug,导致没有往 response 里写入内容就返回,这会造成客户端阻塞等待,所以当出现错误提前返回时,需要一个默认的错误处理函数,给客户端返回默认错误信息。

import (
    "net/http"

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

很多时候,执行路由对应 handler 时我们并不想直接操作 http.responseWriter 和 *http.request,并且希望有一些简单的封装,提供更多的功能。再者,这两个对象并不能很好的携带中间件处理过程中产生的一些参数。所以我们会定义一个 Context (下一节)来封装它们。每一个请求都应该有一个 Context,为了方便的管理,使用 sync.Pool 做一个 context 池。

创建新的 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 注册路由,由于使用 gorilla.mux,调用其 HandleFunc ,返回 router 本身,在调用 Method 即可指定请求方法。不过我们还可以在自己的 handler 执行之前,提供一些钩子,这里我们可以添加一些 filter 函数,以便功能扩展。

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

前面提到可以用一个 Context 包装 http.responseWriter 和 *http.request,并且提供一些额外的功能。额外的功能如 validator,用来对请求做参数验证。这个 validator 我们可以直接用一个第三方库,也可以做成 Interface 以便升级。

另外我们可能需要 Context 能够携带额外的信息,所以可以加一个 map 用来存储。

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

不要忘了在 Router 里面我们是用一个线程安全的池来管理 context ,也就是每次用完 context 需要还回去来避免临时分配带来的开销。所以别忘了还回去之前需要把 context 重置成原来的样子。

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,我们还需要封装一个 server。首先定义一个 EntryPoiont 结构体,当然名字随意。非常确认的是我们需要用到 http 包的 Server,还可以加上可能用到的 net.Listener。另外,我们需要方便的添加一些即插即用的工具,所以需要中间件,这里我使用第三方库 negroni 。然后我们可能需要一个通知关闭所有连接的机制,用一个 channel 可以做到。所以 EntryPoint 大致如下:

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

negroni

其实 negroni 的核心代码也很简单,就只是把多个 middleware 串起来使其能够串行调用。

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)
}

关键就是 Handler 接口,所有第三方实现的中间件要和 negroni 一起用的话,都要实现它,并且每个中间件执行完自己的功能后,要去调用 next 触发下一个中间件的执行。

添加中间件:

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}
}

添加中间件的时候,递归地调用 build ,把所有 middlewares 串起来。必然的,negroni 实现了 http.Handler 接口,这使得 Negroni 可以当做 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

当所有中间件执行完了以后,应该把 context 传给 router 去执行对应的路由,所以把 router 作为最后一个中间件传到 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
}

当然在启动 Server.Serve() 之前,还要把 ep.buildRouter 返回的对象赋给 ep.Server.Handler,使这个对象代替 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
}

接下来就可以调用 start 跑起服务:

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
}

中间件封装

有的时候有一些现成的中间件,但是不能直接放到 negroni 里面用,就需要我们给它加一层封装。

例如,我们要做 jwt 验证,使用第三方的 *jwtmiddleware.JWTMiddleware,但是有的路径我们不需要 token,需要跳过 jwt 中间件。不方便改别人的代码,可以这样封装来代替原来的 *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 作为一个匿名变量,这样可以在自定义的 JWTMiddleware 上直接调用 *jwtmiddleware.JWTMiddleware 的函数。然后用 handler 函数覆盖原有的 HandlerWithNext 函数,这样就能通过调用时传入的 skipper 函数判断是否需要跳过 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)
}

总结

目前为止我们实现了一个简易通用的 HTTP server 框架,虽然功能还不是很完善,不过好在可扩展性比较高,我们可以在此基础上任意扩展,可以添加上缓存、数据库、监控等等模块。

如果有兴趣的话,可以去看看 echo 的实现,其实也是大同小异。

最后,再放一遍项目地址,还有一些别的库,欢迎 star 和 pr 啦!

©著作权归作者所有,转载或内容合作请联系作者
  • 序言:七十年代末,一起剥皮案震惊了整个滨河市,随后出现的几起案子,更是在滨河造成了极大的恐慌,老刑警刘岩,带你破解...
    沈念sama阅读 205,132评论 6 478
  • 序言:滨河连续发生了三起死亡事件,死亡现场离奇诡异,居然都是意外死亡,警方通过查阅死者的电脑和手机,发现死者居然都...
    沈念sama阅读 87,802评论 2 381
  • 文/潘晓璐 我一进店门,熙熙楼的掌柜王于贵愁眉苦脸地迎上来,“玉大人,你说我怎么就摊上这事。” “怎么了?”我有些...
    开封第一讲书人阅读 151,566评论 0 338
  • 文/不坏的土叔 我叫张陵,是天一观的道长。 经常有香客问我,道长,这世上最难降的妖魔是什么? 我笑而不...
    开封第一讲书人阅读 54,858评论 1 277
  • 正文 为了忘掉前任,我火速办了婚礼,结果婚礼上,老公的妹妹穿的比我还像新娘。我一直安慰自己,他们只是感情好,可当我...
    茶点故事阅读 63,867评论 5 368
  • 文/花漫 我一把揭开白布。 她就那样静静地躺着,像睡着了一般。 火红的嫁衣衬着肌肤如雪。 梳的纹丝不乱的头发上,一...
    开封第一讲书人阅读 48,695评论 1 282
  • 那天,我揣着相机与录音,去河边找鬼。 笑死,一个胖子当着我的面吹牛,可吹牛的内容都是我干的。 我是一名探鬼主播,决...
    沈念sama阅读 38,064评论 3 399
  • 文/苍兰香墨 我猛地睁开眼,长吁一口气:“原来是场噩梦啊……” “哼!你这毒妇竟也来了?” 一声冷哼从身侧响起,我...
    开封第一讲书人阅读 36,705评论 0 258
  • 序言:老挝万荣一对情侣失踪,失踪者是张志新(化名)和其女友刘颖,没想到半个月后,有当地人在树林里发现了一具尸体,经...
    沈念sama阅读 42,915评论 1 300
  • 正文 独居荒郊野岭守林人离奇死亡,尸身上长有42处带血的脓包…… 初始之章·张勋 以下内容为张勋视角 年9月15日...
    茶点故事阅读 35,677评论 2 323
  • 正文 我和宋清朗相恋三年,在试婚纱的时候发现自己被绿了。 大学时的朋友给我发了我未婚夫和他白月光在一起吃饭的照片。...
    茶点故事阅读 37,796评论 1 333
  • 序言:一个原本活蹦乱跳的男人离奇死亡,死状恐怖,灵堂内的尸体忽然破棺而出,到底是诈尸还是另有隐情,我是刑警宁泽,带...
    沈念sama阅读 33,432评论 4 322
  • 正文 年R本政府宣布,位于F岛的核电站,受9级特大地震影响,放射性物质发生泄漏。R本人自食恶果不足惜,却给世界环境...
    茶点故事阅读 39,041评论 3 307
  • 文/蒙蒙 一、第九天 我趴在偏房一处隐蔽的房顶上张望。 院中可真热闹,春花似锦、人声如沸。这庄子的主人今日做“春日...
    开封第一讲书人阅读 29,992评论 0 19
  • 文/苍兰香墨 我抬头看了看天上的太阳。三九已至,却和暖如春,着一层夹袄步出监牢的瞬间,已是汗流浃背。 一阵脚步声响...
    开封第一讲书人阅读 31,223评论 1 260
  • 我被黑心中介骗来泰国打工, 没想到刚下飞机就差点儿被人妖公主榨干…… 1. 我叫王不留,地道东北人。 一个月前我还...
    沈念sama阅读 45,185评论 2 352
  • 正文 我出身青楼,却偏偏与公主长得像,于是被迫代替她去往敌国和亲。 传闻我的和亲对象是个残疾皇子,可洞房花烛夜当晚...
    茶点故事阅读 42,535评论 2 343

推荐阅读更多精彩内容

  • Spring Cloud为开发人员提供了快速构建分布式系统中一些常见模式的工具(例如配置管理,服务发现,断路器,智...
    卡卡罗2017阅读 134,598评论 18 139
  • Android 自定义View的各种姿势1 Activity的显示之ViewRootImpl详解 Activity...
    passiontim阅读 171,469评论 25 707
  • 批处理符号简介 回显屏蔽 @ 重定向1 >与>> 重定向2 < 管道符号 | 转义符 ^ 逻辑命令符包括:&、&&...
    wyude阅读 2,963评论 2 5
  • 人活着有意义吗?很多人会想:“怎么会没有意义呢,要是没有生命多枯燥啊,肯定有!” 事实上:“没有!!!" 正因为没...
    关中人阅读 692评论 0 1
  • 路在脚下,梦在心中,心在远方。 你可能时常问自己,人生最远的距离会不会是遥不可及的天际,而我总是说...
    艾薇儿_0c3d阅读 220评论 0 3