golang学习之negroni/gizp源码分析

来源:互联网 发布:djvu转pdf软件 编辑:程序博客网 时间:2024/05/18 10:46

  在 Go 语言里,Negroni 是一个很地道的 Web 中间件,它是一个具备微型、非嵌入式、鼓励使用原生 net/http 库特征的中间件。利用它地Use功能,我们可以很简单地自定义中间件并使用。其中,gzip就是一个很好地例子,它实现了服务器对gzip的响应。
  我们可以通过一个简单的例子,来了解gzip的使用:

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

如果你想了解更多关于上面定义的概念,请参考:

  • compress/gzip:go语言学习之gzip包解读
  • WebSocket:WebSocket 是什么原理?为什么可以实现持久连接?
  • Vary头部:http vary 头的作用
//gzipResponseWriter包含了gzip.Writer以及negroni.ResponseWriter//同时还有一个变量判断头部是否已经被设置type gzipResponseWriter struct {    w *gzip.Writer    negroni.ResponseWriter    wroteHeader bool}//建立一个临时对象池,用于存放gzip.Writertype 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中间件的编写方法,那么也可以尝试自己编写一些中间件了。

原创粉丝点击