美文网首页Go语言
GO http server (III) 组建简易 HTTP S

GO http server (III) 组建简易 HTTP S

作者: 小小小超子 | 来源:发表于2018-06-02 15:37 被阅读23次

上篇提到 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 啦!

相关文章

网友评论

    本文标题:GO http server (III) 组建简易 HTTP S

    本文链接:https://www.haomeiwen.com/subject/icyrsftx.html