创建和使用 HTTP 中间件层

在构建 Web 应用时,可能需要为许多(甚至全部)HTTP 请求创建一些共享的功能。你可能需要记录每个请求,对每个响应进行 gzip 压缩,或者在进行重大处理之前检查缓存信息。

一种创建这些共享的功能的方法是创建中间件层 - 自包含代码,它们在正常应用处理之前或之后独立处理请求。在 Go 中,使用中间件的常见位置在 ServeMux 和应用处理程序之间,总的来说,对 HTTP 请求的控制流程如下所示:

ServeMux => 中间件处理程序 => 应用处理程序

在这篇文章中,我将解释如何使自定义中间件在这种模式下工作,以及如何使用第三方中间软件包的一些具体示例。

基础原则(The Basic Principles)

在 Go 中创建和使用中间件层很简单。我们想实现:

  • 我们的中间件层,应当实现 http.Handler 接口。
  • 构建一个包含我们的中间件处理程序和我们的普通应用处理程序的处理程序链条,我们可以使用它来注册一个 http.ServeMux。

我现在就会解释。

希望你已经熟悉下面的构造处理程序的方法(如果没有,最好在继续阅读之前阅读这个底层实现)。

func messageHandler(message string) http.Handler {
  return http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
    w.Write([]byte(message)
  })
}

在这个 handler 中,我们在一个匿名的函数和一个跨越闭包的 message 变量组成的闭包中放上了自己的逻辑(一个简单的 w.Write 函数)。 然后,然后我们通过 http.HandlerFunc 适配器将其返回,将此闭包转换为处理程序。

我们可以用同样的方法创建一条 handler 链。与其(向上面一样)传递一个字符串给闭包,我们可以传递一个 链中的 next handler 作为变量,然后通过调用它的 ServeHTTP() 方法转换控制给 next handler。

这给了我们一个重构中间件完整的模式:

func exampleMiddleware(next http.Handler) http.Handler {
  return http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
    // Our middleware logic goes here...
    next.ServeHTTP(w, r)
  })
}

你会注意到这个中间件函数有一个 func(http.Handler) http.Handler 签名。它接受一个 handler 作为参数,并返回一个 handler。这很有用,有如下两个理由:

  • 因为它返回一个 handler,我们可以直接使用 net/http 包提供的标准的 ServeMux 注册中间件函数。
  • 通过将中间件功能嵌套在一起,我们可以创建一个任意长的处理程序链。例如:

http.Handle("/", middlewareOne(middlewareTwo(finalHandler)))

控制流程说明(Illustrating the Flow of Control)

让我们看一个使用了一些中间层的 stripper-down 的例子,它简单地将日志信息打到标准输出中:

File: main.go
package main

import (
  "log"
  "net/http"
)

func middlewareOne(next http.Handler) http.Handler {
  return http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
    log.Println("Executing middlewareOne")
    next.ServeHTTP(w, r)
    log.Println("Executing middlewareOne again")
  })
}

func middlewareTwo(next http.Handler) http.Handler {
  return http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
    log.Println("Executing middlewareTwo")
    if r.URL.Path != "/" {
      return
    }
    next.ServeHTTP(w, r)
    log.Println("Executing middlewareTwo again")
  })
}

func final(w http.ResponseWriter, r *http.Request) {
  log.Println("Executing finalHandler")
  w.Write([]byte("OK"))
}

func main() {
  finalHandler := http.HandlerFunc(final)

  http.Handle("/", middlewareOne(middlewareTwo(finalHandler)))
  http.ListenAndServe(":3000", nil)
}

运行这个程序,并发送一个请求给 http://localhost:3000。你应该可以得到类似下面的输出:

$ go run main.go
2014/10/13 20:27:36 Executing middlewareOne
2014/10/13 20:27:36 Executing middlewareTwo
2014/10/13 20:27:36 Executing finalHandler
2014/10/13 20:27:36 Executing middlewareTwo again
2014/10/13 20:27:36 Executing middlewareOne again

很明显地可以看到,如何通过处理程序链,按照嵌套的顺序传递控制权,然后再以相反的顺序返回。

我们可以在任何时候,通过在中间件层 return 来停止 handler 链的传递。

在上面的例子中,我在 middlewareTwo 函数中嵌入了一个返回的条件。可以通过访问 http://localhost:3000/foo,并再次检查日志来验证 - 这一次,你可以看到在访问 middlerwareTwo 之后就结束了,没有按顺序从链中返回。

理解了。再给一个合适的例子如何?

好吧,我们创建了一个处理一个包含 XML 体的请求服务器。我们想创建一些中间层,a)检查请求体是否存在,b)嗅探请求体是否是 XML 格式。如果其中的某一项检查失败,我们希望我们的中间件返回一个错误的信息并让请求停止去让我们的应用处理。

File: main.go
package main

import (
  "bytes"
  "net/http"
)

func enforceXMLHandler(next http.Handler) http.Handler {
  return http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
    // Check for a request body
    if r.ContentLength == 0 {
      http.Error(w, http.StatusText(400), 400)
      return
    }
    // Check its MIME type
    buf := new(bytes.Buffer)
    buf.ReadFrom(r.Body)
    if http.DetectContentType(buf.Bytes()) != "text/xml; charset=utf-8" {
      http.Error(w, http.StatusText(415), 415)
      return
    }
    next.ServeHTTP(w, r)
  })
}

func main() {
  finalHandler := http.HandlerFunc(final)

  http.Handle("/", enforceXMLHandler(finalHandler))
  http.ListenAndServe(":3000", nil)
}

func final(w http.ResponseWriter, r *http.Request) {
  w.Write([]byte("OK"))
}

这看起来很好。让我们用一个简单的 XML 文件来做测试:

$ cat > books.xml
<?xml version="1.0"?>
<books>
  <book>
    <author>H. G. Wells</author>
    <title>The Time Machine</title>
    <price>8.50</price>
  </book>
</books>

并用 CURL 发送一些请求:

$ curl -i localhost:3000
HTTP/1.1 400 Bad Request
Content-Type: text/plain; charset=utf-8
Content-Length: 12

Bad Request
$ curl -i -d "This is not XML" localhost:3000
HTTP/1.1 415 Unsupported Media Type
Content-Type: text/plain; charset=utf-8
Content-Length: 23

Unsupported Media Type
$ curl -i -d @books.xml localhost:3000
HTTP/1.1 200 OK
Date: Fri, 17 Oct 2014 13:42:10 GMT
Content-Length: 2
Content-Type: text/plain; charset=utf-8

OK

使用第三方中间件(Using Third-Party Middleware)

你可能想要使用第三方软件包而不是自己的中间件。在这里,我们看到了一对: goji/httpauth 和 Gorilla 的 LoggingHandler。

goji/httpauth 包提供了 HTTP 基本认证的功能。它有一个 SimpleBasicAuth helper,它返回一个带有 func (http.Handler) http.Handler 签名的函数。这意味着我们可以像我们定制的中间件一样的方式使用它。

$ go get github.com/goji/httpauth
File: main.go
package main

import (
  "github.com/goji/httpauth"
  "net/http"
)

func main() {
  finalHandler := http.HandlerFunc(final)
  authHandler := httpauth.SimpleBasicAuth("username", "password")

  http.Handle("/", authHandler(finalHandler))
  http.ListenAndServe(":3000", nil)
}

func final(w http.ResponseWriter, r *http.Request) {
  w.Write([]byte("OK"))
}

如果你运行这个例子,你应该获取到了你idai的回应,有效和无效的认证:

$ curl -i username:password@localhost:3000
HTTP/1.1 200 OK
Content-Length: 2
Content-Type: text/plain; charset=utf-8

OK
$ curl -i username:wrongpassword@localhost:3000
HTTP/1.1 401 Unauthorized
Content-Type: text/plain; charset=utf-8
Www-Authenticate: Basic realm=""Restricted""
Content-Length: 13

Unauthorized

Gorilla 的 LogginHandler - 记录 Apache 风格的日志 - 有一点不同。

它使用 func(out io.Writer, h http.Handler) http.Handler 签名,它不仅需要 next handler,而且还需要将日志写入的 io.Writer

以下是一个简单的例子,我们将日志写入到 server.log 文件中:

go get github.com/gorilla/handlers
File: main.go
package main

import (
  "github.com/gorilla/handlers"
  "net/http"
  "os"
)

func main() {
  finalHandler := http.HandlerFunc(final)

  logFile, err := os.OpenFile("server.log", os.O_WRONLY|os.O_CREATE|os.O_APPEND, 0666)
  if err != nil {
    panic(err)
  }

  http.Handle("/", handlers.LoggingHandler(logFile, finalHandler))
  http.ListenAndServe(":3000", nil)
}

func final(w http.ResponseWriter, r *http.Request) {
  w.Write([]byte("OK"))
}

在这样的例子中,我们的代码非常清晰。但是如果我们像使用 LoggingHandler 作为更大的中间件链的一部分,会发生什么呢?我们可以很容易地得到一个看起来像这样地声明:

http.Handle("/", handlers.LoggingHandler(logFile, authHandler(enforceXMLHandler(finalHandler))))

… 这让我的头疼!

一个可以让它变得清晰的方法是,创建一个结构体函数(称之为 myLoggingHandler)带着这样的声明 func(http.Handler) http.Handler。这将使我们能够与其他中间件更加整洁地嵌套在一起:

func myLoggingHandler(h http.Handler) http.Handler {
  logFile, err := os.OpenFile("server.log", os.O_WRONLY|os.O_CREATE|os.O_APPEND, 0666)
  if err != nil {
    panic(err)
  }
  return handlers.LoggingHandler(logFile, h)
}

func main() {
  finalHandler := http.HandlerFunc(final)

  http.Handle("/", myLoggingHandler(finalHandler))
  http.ListenAndServe(":3000", nil)
}

如果你运行这个应用,并发送一些请求(给它),你的 server.log 文件应该有如下内容:

$ cat server.log
127.0.0.1 - - [21/Oct/2014:18:56:43 +0100] "GET / HTTP/1.1" 200 2
127.0.0.1 - - [21/Oct/2014:18:56:36 +0100] "POST / HTTP/1.1" 200 2
127.0.0.1 - - [21/Oct/2014:18:56:43 +0100] "PUT / HTTP/1.1" 200 2

如果你感兴趣,下面的例子包含了这篇文章中 three middleware handlers 的要点

作为附注:请注意,Gorilla LoggingHandler 记录了日志中的响应状态(200)和响应长度(2).这很有趣。上游的日志记录中间件是如何知道我们的应用处理程序返回的响应体的?

它通过定义自己的 responseLogger 类型来包装 http.ResponseWriter,并创建自定义的 responseLogger.Write() 和 responseLogger.WriteHeader() 方法。这些方法不仅可以返回响应,还可以存储(响应的)大小和状态供以后检查。Gorilla 的 LoggingHandler 将 responseLogger 传递给链中的下一个处理程序,而不是普通的 http.ResponseWriter。

其他工具(Additional Tools)

Justinas Stankevičius 切片 是一个非常聪明而又非常轻量级的包,它为链接中间件处理程序提供了一些语法糖。在最基础部分,Alice 允许你重写它:

http.Handle("/", myLoggingHandler(authHandler(enforceXMLHandler(finalHandler))))

像这样:

http.Handle("/", alice.New(myLoggingHandler, authHandler, enforceXMLHandler).Then(finalHandler))

阅读更多
文章标签: 中间件
想对作者说点什么? 我来说一句

没有更多推荐了,返回首页

关闭
关闭
关闭