关于godailylib:Go-每日一库之-nethttp基础和中间件

5次阅读

共计 11123 个字符,预计需要花费 28 分钟才能阅读完成。

简介

简直所有的编程语言都以 Hello World 作为入门程序的示例,其中有一部分以编写一个 Web 服务器作为实战案例的开始。每种编程语言都有很多用于编写 Web 服务器的库,或以规范库,或通过第三方库的形式提供。Go 语言也不例外。本文及后续的文章就去摸索 Go 语言中的各个 Web 编程框架,它们的根本应用,浏览它们的源码,比拟它们优缺点。让咱们先从 Go 语言的规范库 net/http 开始。规范库 net/http 让编写 Web 服务器的工作变得非常简单。咱们一起摸索如何应用 net/http 库实现一些常见的性能或模块,理解这些对咱们学习其余的库或框架将会很有帮忙。

Hello World

应用 net/http 编写一个简略的 Web 服务器非常简单:

package main

import (
  "fmt"
  "net/http"
)

func index(w http.ResponseWriter, r *http.Request) {fmt.Fprintln(w, "Hello World")
}

func main() {http.HandleFunc("/", index)
  http.ListenAndServe(":8080", nil)
}

首先,咱们调用 http.HandleFunc("/", index) 注册门路处理函数,这里将门路 / 的处理函数设置为index。处理函数的类型必须是:

func (http.ResponseWriter, *http.Request)

其中 *http.Request 示意 HTTP 申请对象,该对象蕴含申请的所有信息,如 URL、首部、表单内容、申请的其余内容等。

http.ResponseWriter是一个接口类型:

// net/http/server.go
type ResponseWriter interface {Header() Header
  Write([]byte) (int, error)
  WriteHeader(statusCode int)
}

用于向客户端发送响应,实现了 ResponseWriter 接口的类型显然也实现了 io.Writer 接口。所以在处理函数 index 中,能够调用 fmt.Fprintln()ResponseWriter写入响应信息。

仔细阅读 net/http 包中 HandleFunc() 函数的源码:

func HandleFunc(pattern string, handler func(ResponseWriter, *Request)) {DefaultServeMux.HandleFunc(pattern, handler)
}

咱们发现它间接调用了一个名为 DefaultServeMux 对象的 HandleFunc() 办法。DefaultServeMuxServeMux 类型的实例:

type ServeMux struct {
  mu    sync.RWMutex
  m     map[string]muxEntry
  es    []muxEntry // slice of entries sorted from longest to shortest.
  hosts bool       // whether any patterns contain hostnames
}

var DefaultServeMux = &defaultServeMux
var defaultServeMux ServeMux

像这种提供默认类型实例的用法在 Go 语言的各个库中十分常见,在默认参数就曾经足够的场景中应用默认实现很不便 ServeMux 保留了注册的所有门路和处理函数的对应关系。ServeMux.HandleFunc()办法如下:

func (mux *ServeMux) HandleFunc(pattern string, handler func(ResponseWriter, *Request)) {mux.Handle(pattern, HandlerFunc(handler))
}

这里将处理函数 handler 转为 HandlerFunc 类型,而后调用 ServeMux.Handle() 办法注册。留神这里的 HandlerFunc(handler) 是类型转换,而非函数调用,类型 HandlerFunc 的定义如下:

type HandlerFunc func(ResponseWriter, *Request)

func (f HandlerFunc) ServeHTTP(w ResponseWriter, r *Request) {f(w, r)
}

HandlerFunc实际上是以函数类型 func(ResponseWriter, *Request) 为底层类型,为 HandlerFunc 类型定义了办法 ServeHTTP。是的,Go 语言容许为(基于)函数的类型定义办法。Serve.Handle() 办法只承受类型为接口 Handler 的参数:

type Handler interface {ServeHTTP(ResponseWriter, *Request)
}

func (mux *ServeMux) Handle(pattern string, handler Handler) {
  if mux.m == nil {mux.m = make(map[string]muxEntry)
  }
  e := muxEntry{h: handler, pattern: pattern}
  if pattern[len(pattern)-1] == '/' {mux.es = appendSorted(mux.es, e)
  }
  mux.m[pattern] = e
}

显然 HandlerFunc 实现了接口 HandlerHandlerFunc 类型只是为了不便注册函数类型的处理器。咱们当然能够间接定义一个实现 Handler 接口的类型,而后注册该类型的实例:

type greeting string

func (g greeting) ServeHTTP(w http.ResponseWriter, r *http.Request) {fmt.Fprintln(w, g)
}

http.Handle("/greeting", greeting("Welcome, dj"))

咱们基于 string 类型定义了一个新类型 greeting,而后为它定义一个办法ServeHTTP()(实现接口Handler),最初调用http.Handle() 办法注册该处理器。

为了便于辨别,咱们将通过 HandleFunc() 注册的称为处理函数,将通过 Handle() 注册的称为处理器。通过下面的源码剖析不难看出,它们在底层实质上是一回事。

注册了解决逻辑后,调用 http.ListenAndServe(":8080", nil) 监听本地计算机的 8080 端口,开始解决申请。上面看源码的解决:

func ListenAndServe(addr string, handler Handler) error {server := &Server{Addr: addr, Handler: handler}
  return server.ListenAndServe()}

ListenAndServe创立了一个 Server 类型的对象:

type Server struct {
  Addr string
  Handler Handler
  TLSConfig *tls.Config
  ReadTimeout time.Duration
  ReadHeaderTimeout time.Duration
  WriteTimeout time.Duration
  IdleTimeout time.Duration
}

Server构造体有比拟多的字段,咱们能够应用这些字段来调节 Web 服务器的参数,如下面的 ReadTimeout/ReadHeaderTimeout/WriteTimeout/IdleTimeout 用于管制读写和闲暇超时。在该办法中,先调用 net.Listen() 监听端口,将返回的 net.Listener 作为参数调用 Server.Serve() 办法:

func (srv *Server) ListenAndServe() error {
  addr := srv.Addr
  ln, err := net.Listen("tcp", addr)
  if err != nil {return err}
  return srv.Serve(ln)
}

Server.Serve() 办法中,应用一个有限的 for 循环,不停地调用 Listener.Accept() 办法承受新连贯,开启新 goroutine 解决新连贯:

func (srv *Server) Serve(l net.Listener) error {
  var tempDelay time.Duration // how long to sleep on accept failure
  for {rw, err := l.Accept()
    if err != nil {if ne, ok := err.(net.Error); ok && ne.Temporary() {
        if tempDelay == 0 {tempDelay = 5 * time.Millisecond} else {tempDelay *= 2}
        if max := 1 * time.Second; tempDelay > max {tempDelay = max}
        srv.logf("http: Accept error: %v; retrying in %v", err, tempDelay)
        time.Sleep(tempDelay)
        continue
      }
      return err
    }
    tempDelay = 0
    c := srv.newConn(rw)
    go c.serve(connCtx)
  }
}

这里有一个 指数退却策略 的用法。如果 l.Accept() 调用返回谬误,咱们判断该谬误是不是临时性地(ne.Temporary())。如果是临时性谬误,Sleep一小段时间后重试,每产生一次临时性谬误,Sleep的工夫翻倍,最多 Sleep 1s。取得新连贯后,将其封装成一个conn 对象(srv.newConn(rw)),创立一个 goroutine 运行其 serve() 办法。省略无关逻辑的代码如下:

func (c *conn) serve(ctx context.Context) {
  for {w, err := c.readRequest(ctx)
    serverHandler{c.server}.ServeHTTP(w, w.req)
    w.finishRequest()}
}

serve()办法其实就是不停地读取客户端发送地申请,创立 serverHandler 对象调用其 ServeHTTP() 办法去解决申请,而后做一些清理工作。serverHandler只是一个两头的辅助构造,代码如下:

type serverHandler struct {srv *Server}

func (sh serverHandler) ServeHTTP(rw ResponseWriter, req *Request) {
  handler := sh.srv.Handler
  if handler == nil {handler = DefaultServeMux}
  handler.ServeHTTP(rw, req)
}

Server 对象中获取 Handler,这个Handler 就是调用 http.ListenAndServe() 时传入的第二个参数。在 Hello World 的示例代码中,咱们传入了 nil。所以这里handler 会取默认值 DefaultServeMux。调用DefaultServeMux.ServeHTTP() 办法解决申请:

func (mux *ServeMux) ServeHTTP(w ResponseWriter, r *Request) {h, _ := mux.Handler(r)
  h.ServeHTTP(w, r)
}

mux.Handler(r)通过申请的门路信息查找处理器,而后调用处理器的 ServeHTTP() 办法解决申请:

func (mux *ServeMux) Handler(r *Request) (h Handler, pattern string) {host := stripHostPort(r.Host)
  return mux.handler(host, r.URL.Path)
}

func (mux *ServeMux) handler(host, path string) (h Handler, pattern string) {h, pattern = mux.match(path)
  return
}

func (mux *ServeMux) match(path string) (h Handler, pattern string) {v, ok := mux.m[path]
  if ok {return v.h, v.pattern}

  for _, e := range mux.es {if strings.HasPrefix(path, e.pattern) {return e.h, e.pattern}
  }
  return nil, ""
}

下面的代码省略了大量的无关代码,在 match 办法中,首先会查看门路是否准确匹配 mux.m[path]。如果不能准确匹配,前面的for 循环会匹配门路的最长前缀。只有注册了 / 根门路解决,所有未匹配到的门路最终都会交给 / 门路解决 。为了保障最长前缀优先,在注册时,会对门路进行排序。所以mux.es 中寄存的是按门路排序的解决列表:

func appendSorted(es []muxEntry, e muxEntry) []muxEntry {n := len(es)
  i := sort.Search(n, func(i int) bool {return len(es[i].pattern) < len(e.pattern)
  })
  if i == n {return append(es, e)
  }
  es = append(es, muxEntry{})
  copy(es[i+1:], es[i:])
  es[i] = e
  return es
}

运行,在浏览器中键入网址localhost:8080,能够看到网页显示Hello World。键入网址localhost:8080/greeting,看到网页显示Welcome, dj

思考题:
依据最长前缀的逻辑,如果键入 localhost:8080/greeting/a/b/c,应该会匹配/greeting 门路。
如果键入 localhost:8080/a/b/c,应该会匹配/ 门路。是这样么?答案放在前面😀。

创立ServeMux

调用 http.HandleFunc()/http.Handle() 都是将处理器 / 函数注册到 ServeMux 的默认对象 DefaultServeMux 上。应用默认对象有一个问题:不可控。

一来 Server 参数都应用了默认值,二来第三方库也可能应用这个默认对象注册一些解决,容易抵触。更重大的是,咱们在不知情中调用 http.ListenAndServe() 开启 Web 服务,那么第三方库注册的解决逻辑就能够通过网络拜访到,有极大的安全隐患。所以,除非在示例程序中,否则倡议不要应用默认对象。

咱们能够应用 http.NewServeMux() 创立一个新的 ServeMux 对象,而后创立 http.Server 对象定制参数,用 ServeMux 对象初始化 ServerHandler字段,最初调用 Server.ListenAndServe() 办法开启 Web 服务:

func main() {mux := http.NewServeMux()
  mux.HandleFunc("/", index)
  mux.Handle("/greeting", greeting("Welcome to go web frameworks"))

  server := &http.Server{
    Addr:         ":8080",
    Handler:      mux,
    ReadTimeout:  20 * time.Second,
    WriteTimeout: 20 * time.Second,
  }
  server.ListenAndServe()}

这个程序与下面的 Hello World 性能基本相同,咱们还额定设置了读写超时。

为了便于了解,我画了两幅图,其实整顿下来整个流程也不简单:

中间件

有时候须要在申请解决代码中减少一些通用的逻辑,如统计解决耗时、记录日志、捕捉宕机等等。如果在每个申请处理函数中增加这些逻辑,代码很快就会变得不可保护,增加新的处理函数也会变得十分繁琐。所以就有了中间件的需要。

中间件有点像面向切面的编程思维,然而与 Java 语言不同。在 Java 中,通用的解决逻辑(也能够称为切面)能够通过反射插入到失常逻辑的解决流程中,在 Go 语言中根本不这样做。

在 Go 中,中间件是通过函数闭包来实现的。Go 语言中的函数是第一类值,既能够作为参数传给其余函数,也能够作为返回值从其余函数返回。咱们后面介绍了处理器 / 函数的应用和实现。那么能够利用闭包封装已有的处理函数。

首先,基于函数类型 func(http.Handler) http.Handler 定义一个中间件类型:

type Middleware func(http.Handler) http.Handler

接下来咱们来编写中间件,最简略的中间件就是在申请前后各输入一条日志:

func WithLogger(handler http.Handler) http.Handler {return http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {logger.Printf("path:%s process start...\n", r.URL.Path)
    defer func() {logger.Printf("path:%s process end...\n", r.URL.Path)
    }()
    handler.ServeHTTP(w, r)
  })
}

实现很简略,通过中间件封装原来的处理器对象,而后返回一个新的处理函数。在新的处理函数中,先输入开始解决的日志,而后用 defer 语句在函数完结后输入解决完结的日志。接着调用原处理器对象的 ServeHTTP() 办法执行原解决逻辑。

相似地,咱们再来实现一个统计解决耗时的中间件:

func Metric(handler http.Handler) http.HandlerFunc {return func (w http.ResponseWriter, r *http.Request) {start := time.Now()
    defer func() {logger.Printf("path:%s elapsed:%fs\n", r.URL.Path, time.Since(start).Seconds())
    }()
    time.Sleep(1 * time.Second)
    handler.ServeHTTP(w, r)
  }
}

Metric中间件封装原处理器对象,开始执行前记录时间,执行实现后输入耗时。为了能不便看到后果,我在下面代码中增加了一个 time.Sleep() 调用。

最初,因为申请的解决逻辑都是由性能开发人员(而非库作者)本人编写的,所以为了 Web 服务器的稳固,咱们须要捕捉可能呈现的 panic。PanicRecover中间件如下:

func PanicRecover(handler http.Handler) http.Handler {return http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {defer func() {if err := recover(); err != nil {logger.Println(string(debug.Stack()))
      }
    }()

    handler.ServeHTTP(w, r)
  })
}

调用 recover() 函数捕捉 panic,输入堆栈信息,为了避免程序异样退出。实际上,在 conn.serve() 办法中也有recover(),程序个别不会异样退出。然而自定义的中间件能够增加咱们本人的定制逻辑。

当初咱们能够这样来注册处理函数:

mux.Handle("/", PanicRecover(WithLogger(Metric(http.HandlerFunc(index)))))
mux.Handle("/greeting", PanicRecover(WithLogger(Metric(greeting("welcome, dj")))))

这种形式略显繁琐,咱们能够编写一个帮忙函数,它承受原始的处理器对象,和可变的多个中间件。对处理器对象利用这些中间件,返回新的处理器对象:

func applyMiddlewares(handler http.Handler, middlewares ...Middleware) http.Handler {for i := len(middlewares)-1; i >= 0; i-- {handler = middlewares[i](handler)
  }

  return handler
}

留神利用程序是 从右到左 的,即 右联合,越凑近原处理器的越晚执行。

利用帮忙函数,注册能够简化为:

middlewares := []Middleware{
  PanicRecover,
  WithLogger,
  Metric,
}
mux.Handle("/", applyMiddlewares(http.HandlerFunc(index), middlewares...))
mux.Handle("/greeting", applyMiddlewares(greeting("welcome, dj"), middlewares...))

下面每次注册解决逻辑都须要调用一次 applyMiddlewares() 函数,还是略显繁琐。咱们能够这样来优化,封装一个本人的 ServeMux 构造,而后定义一个办法 Use() 将中间件保留下来,重写 Handle/HandleFunc 将传入的 http.HandlerFunc/http.Handler 处理器包装中间件之后再传给底层的 ServeMux.Handle() 办法:

type MyMux struct {
  *http.ServeMux
  middlewares []Middleware}

func NewMyMux() *MyMux {
  return &MyMux{ServeMux: http.NewServeMux(),
  }
}

func (m *MyMux) Use(middlewares ...Middleware) {m.middlewares = append(m.middlewares, middlewares...)
}

func (m *MyMux) Handle(pattern string, handler http.Handler) {handler = applyMiddlewares(handler, m.middlewares...)
  m.ServeMux.Handle(pattern, handler)
}

func (m *MyMux) HandleFunc(pattern string, handler http.HandlerFunc) {newHandler := applyMiddlewares(handler, m.middlewares...)
  m.ServeMux.Handle(pattern, newHandler)
}

注册时只须要创立 MyMux 对象,调用其 Use() 办法传入要利用的中间件即可:

middlewares := []Middleware{
  PanicRecover,
  WithLogger,
  Metric,
}
mux := NewMyMux()
mux.Use(middlewares...)
mux.HandleFunc("/", index)
mux.Handle("/greeting", greeting("welcome, dj"))

这种形式简略易用,然而也有它的问题,最大的问题是必须先设置好中间件,而后能力调用 Handle/HandleFunc 注册,后增加的中间件不会对之前注册的处理器 / 函数失效。

为了解决这个问题,咱们能够改写 ServeHTTP 办法,在确定了处理器之后再利用中间件。这样后续增加的中间件也能失效。很多第三方库都是采纳这种形式。http.ServeMux默认的 ServeHTTP() 办法如下:

func (m *ServeMux) ServeHTTP(w http.ResponseWriter, r *http.Request) {
  if r.RequestURI == "*" {if r.ProtoAtLeast(1, 1) {w.Header().Set("Connection", "close")
    }
    w.WriteHeader(http.StatusBadRequest)
    return
  }
  h, _ := m.Handler(r)
  h.ServeHTTP(w, r)
}

革新这个办法定义 MyMux 类型的 ServeHTTP() 办法也很简略,只须要在 m.Handler(r) 获取处理器之后,利用以后的中间件即可:

func (m *MyMux) ServeHTTP(w http.ResponseWriter, r *http.Request) {
  // ...
  h, _ := m.Handler(r)
  // 只须要加这一行即可
  h = applyMiddlewares(h, m.middlewares...)
  h.ServeHTTP(w, r)
}

前面咱们剖析其余 Web 框架的源码时会发现,很多都是相似的做法。为了测试宕机复原,编写一个会触发 panic 的处理函数:

func panics(w http.ResponseWriter, r *http.Request) {panic("not implemented")
}

mux.HandleFunc("/panic", panics)

运行,在浏览器中申请 localhost:8080/localhost:8080/greeting,最初申请 localhost:8080/panic 触发 panic:


思考题

思考题:

这其实就是看浏览代码是不是认真,最长前缀的排序列表在 ServeMux.Handle() 办法中生成:

func (mux *ServeMux) Handle(pattern string, handler Handler) {if pattern[len(pattern)-1] == '/' {mux.es = appendSorted(mux.es, e)
  }
}

这里显著有个限度条件,即注册门路最初必须以 / 结尾才会触发。所以 localhost:8080/greeting/a/b/clocalhost:8080/a/b/c都只会匹配 / 门路。如果想要让 localhost:8080/greeting/a/b/c 匹配门路/greeting,注册门路须要改为/greeting/

http.Handle("/greeting/", greeting("Welcome to go web frameworks"))

这时申请门路 /greeting 会主动重定向(301)到/greeting/

总结

本文介绍了应用规范库 net/http 创立 Web 服务器的根本流程,一步步剖析源码。而后介绍了如何应用中间件简化通用的解决逻辑。学习并了解了 net/http 库的内容对于学习其余的 Go Web 框架十分有帮忙。第三方的 Go Web 框架大多也是基于 net/http 实现本人的 ServeMux 对象而已。

大家如果发现好玩、好用的 Go 语言库,欢送到 Go 每日一库 GitHub 上提交 issue😄

参考

  1. Go 每日一库 GitHub:https://github.com/darjun/go-daily-lib

我的博客:https://darjun.github.io

欢送关注我的微信公众号【GoUpUp】,独特学习,一起提高~

正文完
 0