在 Go 语言里,Negroni 是一个很地道的 Web 中间件,它是一个具备微型、非嵌入式、鼓励使用原生 net/http 库特征的中间件。利用它地Use功能,我们可以很简单地自定义中间件并使用。其中,gzip就是一个很好地例子,它实现了服务器对gzip的响应。
我们可以通过一个简单的例子,来了解gzip的使用:
package main
import (
"fmt"
"net/http"
"github.com/urfave/negroni"
"github.com/phyber/negroni-gzip/gzip"
)
func main() {
mux := http.NewServeMux()
mux.HandleFunc("/", func(w http.ResponseWriter, req *http.Request) {
fmt.Fprintf(w, "Welcome to the home page!")
})
n := negroni.Classic()
n.Use(gzip.Gzip(gzip.DefaultCompression))
n.UseHandler(mux)
n.Run(":3000")
}
你只需要安装对应的包,并运行该程序,然后利用curl工具访问该服务器观察以下结果:
$ curl -H "Accept:gzip" -v http://localhost:3000/
* timeout on name lookup is not supported
* Trying ::1...
% Total % Received % Xferd Average Speed Time Time Time Current
Dload Upload Total Spent Left Speed
0 0 0 0 0 0 0 0 --:--:-- --:--:-- --:--:-- 0* Connected to localhost (::1) port 3000 (#0)
> GET / HTTP/1.1
> Host: localhost:3000
> User-Agent: curl/7.49.1
> Accept:gzip
>
< HTTP/1.1 200 OK
< Date: Thu, 07 Dec 2017 02:07:23 GMT
< Content-Length: 25
< Content-Type: text/plain; charset=utf-8
<
{ [25 bytes data]
100 25 100 25 0 0 1562 0 --:--:-- --:--:-- --:--:-- 1562Welcome to the home page!
* Connection #0 to host localhost left intact
附:linux crul命令指南:http://man.linuxde.net/curl
根据倒数第二行的结果,可以发现,它的确实现了对于gzip流的响应。
想要更加清晰了解本文内容,请先了解negroni对于第三方中间件的使用:
• golang学习之negroni对于第三方中间件的使用分析
gzip中间件是如何实现服务器对于gzip流的响应的呢?可以通过阅读源码来了解。源代码仅有126行,简单易懂。为了方便理解,代码阅读阅读顺序推荐为:
+func Gzip(level int) *handler
|h := &handler{}
|h.pool.New = func() interface{} {...}
+func (h *handler) ServeHTTP(w http.ResponseWriter, r *http.Request, next http.HandlerFunc)
|gz := h.pool.Get().(*gzip.Writer)
|gz.Reset(w)
|nrw := negroni.NewResponseWriter(w)
|grw := gzipResponseWriter{gz, nrw, false}
+func (grw *gzipResponseWriter) WriteHeader(code int)
+func (grw *gzipResponseWriter) Write(b []byte) (int, error)
只有四个函数,是不是很简单呢?我们便开始分析吧。
//const的定义包含了一部分http.Request和http.Response的头部内容
//以及compress/gzip的一部分内容
const (
encodingGzip = "gzip"
headerAcceptEncoding = "Accept-Encoding"
headerContentEncoding = "Content-Encoding"
headerContentLength = "Content-Length"
headerContentType = "Content-Type"
headerVary = "Vary"
headerSecWebSocketKey = "Sec-WebSocket-Key"
BestCompression = gzip.BestCompression
BestSpeed = gzip.BestSpeed
DefaultCompression = gzip.DefaultCompression
NoCompression = gzip.NoCompression
)
如果你想了解更多关于上面定义的概念,请参考:
//gzipResponseWriter包含了gzip.Writer以及negroni.ResponseWriter
//同时还有一个变量判断头部是否已经被设置
type gzipResponseWriter struct {
w *gzip.Writer
negroni.ResponseWriter
wroteHeader bool
}
//建立一个临时对象池,用于存放gzip.Writer
type handler struct {
pool sync.Pool
}
关于临时对象池:go的临时对象池–sync.Pool
该函数返回一个handler处理gzip压缩。但是,这个函数只是创建了一个gzip.Writer,并存放到临时对象池中;真正的处理在ServeHTTP中进行。
func Gzip(level int) *handler {
h := &handler{}
h.pool.New = func() interface{} {
//创建一个gzip.Writer
//丢弃一切向io.Writer的输入并为level赋值
//gz为*gzip.Writer类型
gz, err := gzip.NewWriterLevel(ioutil.Discard, level)
if err != nil {
panic(err)
}
return gz
}
return h
}
关于ioutil.Discard:
ioutil包中的Discard对象实现了接口io.Writer,但是抛弃了所有写入的数据。可以将其当做/dev/null:用于发送需要读取但不想存储的数据。该对象被广泛使用于io.Copy(),目的是耗尽读取端的数据。
任何一个server都有Handler处理器,而只要http.Handler接口的对象都可作为一个处理器。Handler接口定义如下:
type Handler interface {
ServeHTTP(ResponseWriter, *Request)
}
很明显,ServeHTTP是Handler的核心方法。所以,对于gzip中间件来说,ServeHTTP定义了gzip的处理器,即服务器如何处理客户端的gzip流请求。虽然并没有直接实现http.Handler接口,但是negroni封装中实现了它到http.Handler对接。
func (h *handler) ServeHTTP(w http.ResponseWriter, r *http.Request, next http.HandlerFunc) {
//如果请求头部没有包含“Accept:gzip",则跳过gzip压缩
//next Handler有negroni提供;next函数和路由中的HandlerFunc对接,如本文开头例子中输出"Welcome to the home page!"的函数
if !strings.Contains(r.Header.Get(headerAcceptEncoding), encodingGzip) {
next(w, r)
return
}
// 如果客户端请求建立WebSocket连接,则跳过gzip压缩
if len(r.Header.Get(headerSecWebSocketKey)) > 0 {
next(w, r)
return
}
//从对象临时池中取出一个*gzip.Writer,并作类型断言
//使用完毕后,将*gzip.Writer回收
//使用前,重置gzip.Writer
gz := h.pool.Get().(*gzip.Writer)
defer h.pool.Put(gz)
gz.Reset(w)
//创建一个gzipResponseWriter
nrw := negroni.NewResponseWriter(w)
grw := gzipResponseWriter{gz, nrw, false}
//调用next Handler处理gzipResponseWriter
//具体调度由negroni完成
next(&grw, r)
//写入完成,删除headerContentLength的值
grw.Header().Del(headerContentLength)
//关闭
gz.Close()
}
该函数检查Response是否已经预编译,如果已经预编译则在正文写入前禁用gzip.Writer。即如果没有设置Header,则设置Header;否则,等待正文写入。
func (grw *gzipResponseWriter) WriteHeader(code int) {
headers := grw.ResponseWriter.Header()
if headers.Get(headerContentEncoding) == "" {
headers.Set(headerContentEncoding, encodingGzip)
headers.Add(headerVary, headerAcceptEncoding)
} else {
//重置gzip.Writer,并且一切io.Writer的写入都会被丢弃
grw.w.Reset(ioutil.Discard)
grw.w = nil
}
grw.ResponseWriter.WriteHeader(code)
grw.wroteHeader = true
}
该函数把数据写入gzip.Writer,同时设置Content-Type和状态码。
func (grw *gzipResponseWriter) Write(b []byte) (int, error) {
if !grw.wroteHeader {
grw.WriteHeader(http.StatusOK)
}
if grw.w == nil {
return grw.ResponseWriter.Write(b)
}
if len(grw.Header().Get(headerContentType)) == 0 {
grw.Header().Set(headerContentType, http.DetectContentType(b))
}
return grw.w.Write(b)
}
以上便是negroni/gizp源码的所有内容了。通过解读源码,你也已经清楚了negroni中间件的编写方法,那么也可以尝试自己编写一些中间件了。