在构建 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 mainimport ("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 mainimport ("bytes""net/http"
)func enforceXMLHandler(next http.Handler) http.Handler {return http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {// Check for a request bodyif r.ContentLength == 0 {http.Error(w, http.StatusText(400), 400)return}// Check its MIME typebuf := 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: 12Bad 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: 23Unsupported 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-8OK

使用第三方中间件(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 mainimport ("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-8OK
$ 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: 13Unauthorized

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 mainimport ("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))

创建和使用 HTTP 中间件层相关推荐

  1. ASP.NET MVC随想录——创建自定义的Middleware中间件

    经过前2篇文章的介绍,相信大家已经对OWIN和Katana有了基本的了解,那么这篇文章我将继续OWIN和Katana之旅--创建自定义的Middleware中间件. 何为Middleware中间件 M ...

  2. middlewareserver_创建自定义的Middleware中间件

    经过前2篇文章的介绍,相信大家已经对OWIN和Katana有了基本的了解,那么这篇文章我将继续OWIN和Katana之旅--创建自定义的Middleware中间件. 何为Middleware中间件 M ...

  3. 使用CCDirector的notificationNode来创建独立的信息提示层

    我们在游戏的制作中,肯定会设计到呈现给玩家的信息提示,比如获得了一个什么样的技能,获得了多少分数.这样的提示应该独立于游戏画面,即使在场景切换时信息提示也不应该受到影响. cocos2d采用了便于理解 ...

  4. ASP.NET2.0数据操作之创建业务逻辑层

    导言 本教程的第一节所描述的数据访问层(Data Access Layer,以下简称为DAL)已经清晰地将表示逻辑与数据访问逻辑区分开了.不过,即使DAL将数据访问的细节从表示层中分离出来了,可它却不 ...

  5. mxnet创建新的操作(层)

    mxnet创建新的操作(层) 这篇blog将会告诉你如何创建新的MXNet操作(层). 我们竭尽所能提供最好的操作对于绝大多数的使用场景.然而,如果你发现自己需要自定义层,你有3个选择: 1.使用原生 ...

  6. laravel 创建自定义中间件

    首先我们要定义一个新的中间件,Artisan命令: php artisan make:middleware LoginMiddleware 这个命令会在 app/Http/Middleware 目录下 ...

  7. 电商直播平台如何借助容器与中间件实现研发效率提升100%?

    作者:鹿玄,阿里云解决方案架构师 前言 直播带货是近期发展非常迅猛的一种新的电商模式.构建一个电商直播平台从技术角度上大致可以分为视频直播服务.CDN.前端(H5/ 小程序).大数据.以及各种业务后台 ...

  8. Seata阿里分布式事务中间件(一):Seata的基本介绍

    Fescar 是 阿里巴巴 开源的 分布式事务中间件,以 高效 并且对业务 0 侵入 的方式,解决 微服务 场景下面临的分布式事务问题. 什么是微服务化带来的分布式事务问题? 首先,设想一个传统的单体 ...

  9. 中间表增加额外字段_如何定制分表中间件

    前言 一般来说,影响数据库最大的性能问题有两个,一个是对数据库的操作,一个是数据库中的数据太大.对于前者我们可以借助缓存来减少一部分读操作,针对一些复杂的报表分析和搜索可以交给 Hadoop 和 El ...

最新文章

  1. C标准库和glibc(C运行库)的关系
  2. 为什么要两次调用encodeURI来解决乱码问题
  3. 滑动窗口算法_有点难度,几道和「滑动窗口」有关的算法面试题
  4. C++ 学习之旅(16)——虚函数与纯虚函数virtual
  5. 服务器驱动精灵_驱动精灵真的可以帮你安装驱动吗?别再无脑装驱动了
  6. CCSP2020比赛太原理工学子再创佳绩
  7. pdfobject.js和pdf.js的详解
  8. java连接mysql数据库实现图书馆管理系统
  9. Python批量转换png图片为ico
  10. 单机游戏修改游戏数据(你自己就是一个外挂,看完这篇,你一定有不小的收获)
  11. 智能合约安全审计指南
  12. MDK5.30下载来了,含镜像下载地址,ARM同时带来Cortex-M55调试展示(2020-05-05)
  13. [0CTF 2016]piapiapia 1
  14. 密立根油滴实验的c语言程序,密立根油滴实验数据处理程序c++
  15. CH2_数字图像基础
  16. 关于网络下载的记忆碎片
  17. echarts 好看的柱形图
  18. md5等hash算法加密解密问题
  19. NestedTensor(DETR)
  20. 伪类和伪元素的区别及使用场景

热门文章

  1. 中国银行理财产品市场运行态势及投资风险透析报告2021-2027年
  2. 微软宣布Azure DNS全面通用
  3. 上海考生:FRM证书在能享受什么福利?其他地区呢?
  4. 开车总结-2021.7
  5. java.io.FileNotFoundException: file:/xxx/xxx.jar!/BOOT-INF/classes!/xxx.xlsx (没有那个文件或目录)
  6. 马太效应(Matthew Effect)
  7. Hadoop系列之-7、Hadoop3.x的介绍
  8. 使用 AIX TCP/IP 过滤功能设置防火墙
  9. 交叉编译apr和apr-util库
  10. 青龙面板薅羊毛教程之小米电动车