gzip過濾器原始碼分析
本文參考gzip過濾器的原始碼
https://github.com/phyber/negroni-gzip/blob/master/gzip/gzip.go
該過濾器是Negroni(https://github.com/urfave/negroni) 的一箇中間件,gzip是用來壓縮的。
我們關注所有首字母大寫的函式,一共有五個。
func (grw *gzipResponseWriter) WriteHeader(code int)
func (grw *gzipResponseWriter) Write(b []byte) (int, error)
func (rw *gzipResponseWriterCloseNotifier) CloseNotify() <-chan bool
func Gzip(level int) *handle
func (h *handler) ServeHTTP(w http.ResponseWriter, r *http.Request, next http.HandlerFunc)
這主要是因為我們在外部只能呼叫首字母大寫的函式,那我們至少要知道這份程式碼能幫我們幹些什麼。
接下來我們依次自上到下的閱讀理解這些函式。
WriteHeader函式
// Check whether underlying response is already pre-encoded and disable
// gzipWriter before the body gets written, otherwise encoding headers
func (grw *gzipResponseWriter) WriteHeader(code int) {
headers := grw.ResponseWriter.Header()
if headers.Get(headerContentEncoding) == "" {
headers.Set(headerContentEncoding, encodingGzip)
headers.Add(headerVary, headerAcceptEncoding)
} else {
grw.w.Reset(ioutil.Discard)
grw.w = nil
}
// Avoid sending Content-Length header before compression. The length would
// be invalid, and some browsers like Safari will report
// "The network connection was lost." errors
grw.Header().Del(headerContentLength)
grw.ResponseWriter.WriteHeader(code)
grw.wroteHeader = true
}
首先開始的if else語句是用來判斷http相應的頭部“Content-Encoding”一欄是否為空,該欄目代表著http響應的壓縮格式,為空就是還沒壓縮過,那就對它頭部進行設定,設定壓縮方式為“zip”。
若“Content-Encoding”不為空,說明該已經壓縮過了,將grw的Writer置為nil,這也容易理解,既然已經壓縮過了,那麼就不能再壓縮一次了。
然後避免把頭部的“Content-Length”這一欄,把頭部的“Content-Length”這一欄刪掉。
Write函式
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)
}
當沒寫頭部的時候,寫頭部。
若grw的writer為nil,那麼從上一函式可以得知該http響應已經壓縮了,那麼就呼叫ResponseWriter(negroni)進行寫。
若grw的頭部沒寫,那麼就寫上。
最後用gzip的Writer進行寫。
總而言之,這一段函式就兩個返回值,要麼grw的writer為nil,已經壓縮了,那就呼叫ResponseWriter(negroni)進行寫,要麼就沒有壓縮過,那麼就用gzip的writer寫。
CloseNotify函式
func (rw *gzipResponseWriterCloseNotifier) CloseNotify() <-chan bool {
return rw.ResponseWriter.(http.CloseNotifier).CloseNotify()
}
當客戶端關閉時,服務端可以通過該函式返回的一個通道得知連線中斷。
Gzip函式
func Gzip(level int) *handler {
h := &handler{}
h.pool.New = func() interface{} {
gz, err := gzip.NewWriterLevel(ioutil.Discard, level)
if err != nil {
panic(err)
}
return gz
}
return h
}
返回一個handler,這個handler主要在下一個函式用到
ServeHTTP函式
func (h *handler) ServeHTTP(w http.ResponseWriter, r *http.Request, next http.HandlerFunc) {
// Skip compression if the client doesn't accept gzip encoding.
if !strings.Contains(r.Header.Get(headerAcceptEncoding), encodingGzip) {
next(w, r)
return
}
// Skip compression if client attempt WebSocket connection
if len(r.Header.Get(headerSecWebSocketKey)) > 0 {
next(w, r)
return
}
// Retrieve gzip writer from the pool. Reset it to use the ResponseWriter.
// This allows us to re-use an already allocated buffer rather than
// allocating a new buffer for every request.
// We defer g.pool.Put here so that the gz writer is returned to the
// pool if any thing after here fails for some reason (functions in
// next could potentially panic, etc)
gz := h.pool.Get().(*gzip.Writer)
defer h.pool.Put(gz)
gz.Reset(w)
// Wrap the original http.ResponseWriter with negroni.ResponseWriter
// and create the gzipResponseWriter.
nrw := negroni.NewResponseWriter(w)
grw := newGzipResponseWriter(nrw, gz)
// Call the next handler supplying the gzipResponseWriter instead of
// the original.
next(grw, r)
gz.Close()
}
首先是判斷請求的頭部是否有“Accept-Encoding”一欄,若沒有,則說明客戶端不接受壓縮的響應,那我們就不能壓縮響應的報文,直接呼叫下一個中介軟體處理該請求。
其次是判斷請求的頭部是否有“Sec-WebSocket-Key”一欄,如果有,代表客戶端想要進行長連線,那也不能壓縮,呼叫下一個中介軟體處理該請求。
最後就是建立gzipResponseWriter,進行壓縮處理,關閉gzipwriter。
原始碼
以下是gzip原始碼
// Package gzip implements a gzip compression handler middleware for Negroni.
package gzip
import (
"compress/gzip"
"io/ioutil"
"net/http"
"strings"
"sync"
"github.com/urfave/negroni"
)
// These compression constants are copied from the compress/gzip package.
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 is the ResponseWriter that negroni.ResponseWriter is
// wrapped in.
type gzipResponseWriter struct {
w *gzip.Writer
negroni.ResponseWriter
wroteHeader bool
}
// Check whether underlying response is already pre-encoded and disable
// gzipWriter before the body gets written, otherwise encoding headers
func (grw *gzipResponseWriter) WriteHeader(code int) {
headers := grw.ResponseWriter.Header()
if headers.Get(headerContentEncoding) == "" {
headers.Set(headerContentEncoding, encodingGzip)
headers.Add(headerVary, headerAcceptEncoding)
} else {
grw.w.Reset(ioutil.Discard)
grw.w = nil
}
// Avoid sending Content-Length header before compression. The length would
// be invalid, and some browsers like Safari will report
// "The network connection was lost." errors
grw.Header().Del(headerContentLength)
grw.ResponseWriter.WriteHeader(code)
grw.wroteHeader = true
}
// Write writes bytes to the gzip.Writer. It will also set the Content-Type
// header using the net/http library content type detection if the Content-Type
// header was not set yet.
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)
}
type gzipResponseWriterCloseNotifier struct {
*gzipResponseWriter
}
func (rw *gzipResponseWriterCloseNotifier) CloseNotify() <-chan bool {
return rw.ResponseWriter.(http.CloseNotifier).CloseNotify()
}
func newGzipResponseWriter(rw negroni.ResponseWriter, w *gzip.Writer) negroni.ResponseWriter {
wr := &gzipResponseWriter{w: w, ResponseWriter: rw}
if _, ok := rw.(http.CloseNotifier); ok {
return &gzipResponseWriterCloseNotifier{gzipResponseWriter: wr}
}
return wr
}
// handler struct contains the ServeHTTP method
type handler struct {
pool sync.Pool
}
// Gzip returns a handler which will handle the Gzip compression in ServeHTTP.
// Valid values for level are identical to those in the compress/gzip package.
func Gzip(level int) *handler {
h := &handler{}
h.pool.New = func() interface{} {
gz, err := gzip.NewWriterLevel(ioutil.Discard, level)
if err != nil {
panic(err)
}
return gz
}
return h
}
// ServeHTTP wraps the http.ResponseWriter with a gzip.Writer.
func (h *handler) ServeHTTP(w http.ResponseWriter, r *http.Request, next http.HandlerFunc) {
// Skip compression if the client doesn't accept gzip encoding.
if !strings.Contains(r.Header.Get(headerAcceptEncoding), encodingGzip) {
next(w, r)
return
}
// Skip compression if client attempt WebSocket connection
if len(r.Header.Get(headerSecWebSocketKey)) > 0 {
next(w, r)
return
}
// Retrieve gzip writer from the pool. Reset it to use the ResponseWriter.
// This allows us to re-use an already allocated buffer rather than
// allocating a new buffer for every request.
// We defer g.pool.Put here so that the gz writer is returned to the
// pool if any thing after here fails for some reason (functions in
// next could potentially panic, etc)
gz := h.pool.Get().(*gzip.Writer)
defer h.pool.Put(gz)
gz.Reset(w)
// Wrap the original http.ResponseWriter with negroni.ResponseWriter
// and create the gzipResponseWriter.
nrw := negroni.NewResponseWriter(w)
grw := newGzipResponseWriter(nrw, gz)
// Call the next handler supplying the gzipResponseWriter instead of
// the original.
next(grw, r)
gz.Close()
}