当前位置: 首页 > 工具软件 > Negroni > 使用案例 >

golang学习之negroni/gizp源码分析

东郭鹤龄
2023-12-01

  在 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)

  只有四个函数,是不是很简单呢?我们便开始分析吧。

1.数据结构的定义

//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

2. Gzip

  该函数返回一个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(),目的是耗尽读取端的数据。

3. ServeHTTP

  任何一个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()
}

4. WriteHeader

  该函数检查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
}

5. Write

  该函数把数据写入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中间件的编写方法,那么也可以尝试自己编写一些中间件了。

 类似资料: