go語言學習之gzip包解讀

  打開瀏覽器瀏覽網頁時,我們可以看到各種各樣的文字、圖片、視頻等等各式各樣的信息。那麼瀏覽器是怎樣和服務器交互這些信息的呢?通過分析不難發現,這些信息往往要經過統一編碼之後,才進行傳遞。今天,我們來通過分析gzip.go文件,瞭解gzip壓縮的實現,進而去實現自己編碼網頁信息。

數據結構定義

1. 常量定義

常量定義來自deflate定義,閱讀deflate.go文件,可以獲得相關值。這些常量用來標記數據的狀態和壓縮方式。

// These constants are copied from the flate package, so that code that imports
// "compress/gzip" does not also have to import "compress/flate".
const (
    NoCompression      = flate.NoCompression  
    BestSpeed          = flate.BestSpeed
    BestCompression    = flate.BestCompression
    DefaultCompression = flate.DefaultCompression
    HuffmanOnly        = flate.HuffmanOnly
)

deflate.go中對這些常量的定義如下:

const (
    NoCompression      = 0
    BestSpeed          = 1
    BestCompression    = 9
    DefaultCompression = -1
    HuffmanOnly = -2
)

2. Writer結構體的定義

Writer結構體定義了壓縮常用的變量,爲後續接口函數的實現提供了便利。

type Writer struct {
    Header      // written at first call to Write, Flush, or Close
    w           io.Writer
    level       int
    wroteHeader bool
    compressor  *flate.Writer
    digest      uint32 // CRC-32, IEEE polynomial (section 8)
    size        uint32 // Uncompressed size (section 2.3.1)
    closed      bool
    buf         [10]byte
    err         error
}

其中,

  • Header:定義來自gunzip.go,定義了gzip數據頭部的一些信息,稍後我們會詳細解釋
  • w:聲明爲io.Writer接口,調用相關函數完成數據向緩衝區的寫入
  • level:壓縮的等級。範圍爲[-2, 9]
  • wroteHeader:表示gzip頭部信息是否已經填充
  • compressor:壓縮器
  • digest: 32位的循環冗餘校驗位。不懂請點擊這裏
  • size:未壓縮數據的長度
  • closed:該Writer是否關閉
  • buf:存儲gzip頭部的信息
  • err:處理Writer相關函數中的一切錯誤

函數定義

1. NewWriter

該方法創建並使用io.Writer初始化一個Writer。

func NewWriter(w io.Writer) *Writer {
    z, _ := NewWriterLevel(w, DefaultCompression)
    return z
}

2. init

該方法初始化一個Writer。

func (z *Writer) init(w io.Writer, level int) {
    compressor := z.compressor
    if compressor != nil {
        compressor.Reset(w)
    }
    *z = Writer{
        Header: Header{
            OS: 255, // unknown
        },
        w:          w,
        level:      level,
        compressor: compressor,
    }
}

3. NewWriterLevel

該方法創建一個Writer,並且定義了Writer中level的值。

func NewWriterLevel(w io.Writer, level int) (*Writer, error) {
    if level < HuffmanOnly || level > BestCompression {
        return nil, fmt.Errorf("gzip: invalid compression level: %d", level)
    }
    z := new(Writer)
    z.init(w, level)
    return z, nil
}

4. Reset

該方法重置Writer的狀態,從而可以複用一個Writer。

func (z *Writer) Reset(w io.Writer) {
    z.init(w, z.level)
}

5. writeBytes

該方法把一段字節數據的長度寫入z.w。

func (z *Writer) writeBytes(b []byte) error {
    if len(b) > 0xffff {
        return errors.New("gzip.Write: Extra data is too large")
    }
    //var le = binary.LittleEndian
    le.PutUint16(z.buf[:2], uint16(len(b)))
    _, err := z.w.Write(z.buf[:2])
    if err != nil {
        return err
    }
    _, err = z.w.Write(b)
    return err
}
  • 註解1
le.PutUint16(z.buf[:2], uint16(len(b)))

是將16位無符號整數輸出爲小端模式。PutUint16的定義如下:

func (littleEndian) PutUint16(b []byte, v uint16) {
    _ = b[1] // early bounds check to guarantee safety of writes below
    b[0] = byte(v)
    b[1] = byte(v >> 8)
}

關於大端小端模式以及字節序、位順序,請移步這裏

  • 註解2
_, err := z.w.Write(z.buf[:2])

這句話調用了buffer包中的Write函數,定義如下:

func (b *Buffer) Write(p []byte) (n int, err error) {
    b.lastRead = opInvalid
    m, ok := b.tryGrowByReslice(len(p))
    if !ok {
        m = b.grow(len(p))
    }
    return copy(b.buf[m:], p), nil
}

tryGrowByReslice函數是判斷buffer(緩衝區)是否還有足夠的空間寫入p。buffer爲slice類型數據。其定義如下:

func (b *Buffer) tryGrowByReslice(n int) (int, bool) {
    if l := len(b.buf); l+n <= cap(b.buf) {
        b.buf = b.buf[:l+n]
        return l, true
    }
    return 0, false
}

如果空間不足,則利用grow函數分配足夠的空間給buffer。

func (b *Buffer) grow(n int) int {
    m := b.Len()
    // If buffer is empty, reset to recover space.
    if m == 0 && b.off != 0 {
        b.Reset()
    }
    // Try to grow by means of a reslice.
    if i, ok := b.tryGrowByReslice(n); ok {
        return i
    }
    // Check if we can make use of bootstrap array.
    if b.buf == nil && n <= len(b.bootstrap) {
        b.buf = b.bootstrap[:n]
        return 0
    }
    if m+n <= cap(b.buf)/2 {
        // We can slide things down instead of allocating a new
        // slice. We only need m+n <= cap(b.buf) to slide, but
        // we instead let capacity get twice as large so we
        // don't spend all our time copying.
        copy(b.buf[:], b.buf[b.off:])
    } else {
        // Not enough space anywhere, we need to allocate.
        buf := makeSlice(2*cap(b.buf) + n)
        copy(buf, b.buf[b.off:])
        b.buf = buf
    }
    // Restore b.off and len(b.buf).
    b.off = 0
    b.buf = b.buf[:m+n]
    return m
}

6. writeString

該函數將一個UTF-8字符串以GZIP格式寫入z.w。

func (z *Writer) writeString(s string) (err error) {
    // GZIP stores Latin-1 strings; error if non-Latin-1; convert if non-ASCII.
    needconv := false
    for _, v := range s {
        if v == 0 || v > 0xff {
            return errors.New("gzip.Write: non-Latin-1 header string")
        }
        if v > 0x7f {
            needconv = true
        }
    }
    if needconv {
        b := make([]byte, 0, len(s))
        for _, v := range s {
            b = append(b, byte(v))
        }
        _, err = z.w.Write(b)
    } else {
        _, err = io.WriteString(z.w, s)
    }
    if err != nil {
        return err
    }
    // GZIP strings are NUL-terminated.
    z.buf[0] = 0
    _, err = z.w.Write(z.buf[:1])
    return err
}

關於這段代碼,

for _, v := range s {
        if v == 0 || v > 0xff {
            return errors.New("gzip.Write: non-Latin-1 header string")
        }
        if v > 0x7f {
            needconv = true
        }
    }

是判斷string中每個字符是否符合要求。首先,對於ASCII碼,值爲零時即爲’NUL’,也就是char*符號串的結束佔位符’\0’;對於一個byte(unit8類型)最大值不能超過0xff。所以這兩個均不滿足Latin-1 strings要求。需要說明的是,雖然’\0’屬於Latin-1編碼,但是不能出現在string中。關於Latin-1編碼,請點擊這裏
其次,如果string中字符值大於0x7f,也就是127,則需要轉換。對於這裏,我認爲沒有什麼必要,因爲string本質上也爲[]byte。

7. Write

該函數將壓縮數據寫入z.w。

func (z *Writer) Write(p []byte) (int, error) {
    if z.err != nil {
        return 0, z.err
    }
    var n int
    // Write the GZIP header lazily.
    if !z.wroteHeader {
        z.wroteHeader = true
        z.buf = [10]byte{0: gzipID1, 1: gzipID2, 2: gzipDeflate}
        if z.Extra != nil {
            z.buf[3] |= 0x04
        }
        if z.Name != "" {
            z.buf[3] |= 0x08
        }
        if z.Comment != "" {
            z.buf[3] |= 0x10
        }
        if z.ModTime.After(time.Unix(0, 0)) {
            // Section 2.3.1, the zero value for MTIME means that the
            // modified time is not set.
            le.PutUint32(z.buf[4:8], uint32(z.ModTime.Unix()))
        }
        if z.level == BestCompression {
            z.buf[8] = 2
        } else if z.level == BestSpeed {
            z.buf[8] = 4
        }
        z.buf[9] = z.OS
        n, z.err = z.w.Write(z.buf[:10])
        if z.err != nil {
            return n, z.err
        }
        if z.Extra != nil {
            z.err = z.writeBytes(z.Extra)
            if z.err != nil {
                return n, z.err
            }
        }
        if z.Name != "" {
            z.err = z.writeString(z.Name)
            if z.err != nil {
                return n, z.err
            }
        }
        if z.Comment != "" {
            z.err = z.writeString(z.Comment)
            if z.err != nil {
                return n, z.err
            }
        }
        if z.compressor == nil {
            z.compressor, _ = flate.NewWriter(z.w, z.level)
        }
    }
    z.size += uint32(len(p))
    z.digest = crc32.Update(z.digest, crc32.IEEETable, p)
    n, z.err = z.compressor.Write(p)
    return n, z.err
}

要想理解這個函數,需要同時閱讀RFC1952文檔。需要的可以點擊這裏
RFC1952中,對於gzip壓縮數據結構定義如下:
這裏寫圖片描述
這裏寫圖片描述
這裏寫圖片描述
這裏寫圖片描述
這裏寫圖片描述
注:
1. 紅色數字代表對應數據出現的順序
2. “+——-+”表示單個字節
3. “+====+”表示多個字節

我們來逐部分分析代碼:

func (z *Writer) Write(p []byte) (int, error) {
    if z.err != nil { //出現錯誤,則返回0,err
        return 0, z.err
    }
    var n int
    // Write the GZIP header lazily.
    if !z.wroteHeader { //如果壓縮數據頭還沒有賦值
        z.wroteHeader = true //壓縮數據頭設置爲已經賦值
        z.buf = [10]byte{0: gzipID1, 1: gzipID2, 2: gzipDeflate} //首先爲ID1,ID2和CM賦值

+------------關於ID1,ID2,CM值的定義------------------+
const (
    gzipID1     = 0x1f
    gzipID2     = 0x8b
    gzipDeflate = 8
    flagText    = 1 << 0
    flagHdrCrc  = 1 << 1
    flagExtra   = 1 << 2
    flagName    = 1 << 3
    flagComment = 1 << 4
)

ID1和ID2的值是固定的,表示該數據格式爲gzip數據格式;CM(Compression Method)表示數據壓縮方法,CM=1-7數據保留值,CM=8則表示”deflate”壓縮方法。

if z.Extra != nil {
    z.buf[3] |= 0x04
}
if z.Name != "" {
    z.buf[3] |= 0x08
}
if z.Comment != "" {
    z.buf[3] |= 0x10
}

RFC1952的位順序定義如下:
這裏寫圖片描述
7表示高位,0表示低位
FLG的定義如下:
這裏寫圖片描述
所以,如果z.Extra被設置,則bit2爲1,其餘bit置零,即0x04, z.buf[3](初始值爲0x00)與之進行或運算後即可得到一個暫時的FLG值;之後,根據z.Name、z.Comment是否被設置,更新FLG,即可得到最終FLG的值。

//設置MTIME,最後依次修改時間
if z.ModTime.After(time.Unix(0, 0)) { 
    le.PutUint32(z.buf[4:8], uint32(z.ModTime.Unix()))
} 
//設置壓縮level
if z.level == BestCompression {
    z.buf[8] = 2 
} else if z.level == BestSpeed {
    z.buf[8] = 4
}
//設置OS類型
z.buf[9] = z.OS
//buf滿,將數據先寫入下一層Writer的buf,這裏爲io.Writer
n, z.err = z.w.Write(z.buf[:10])
if z.err != nil {
    return n, z.err
}

+---------------關於OS類型的定義--------------+
0 - FAT filesystem (MS-DOS, OS/2, NT/Win32)
1 - Amiga
2 - VMS (or OpenVMS)
3 - Unix
4 - VM/CMS
5 - Atari TOS
6 - HPFS filesystem (OS/2, NT)
7 - Macintosh
8 - Z-System
9 - CP/M
10 - TOPS-20
11 - NTFS filesystem (NT)
12 - QDOS
13 - Acorn RISCOS
255 - unknown

到此爲止,完成了gzip第一部分向下一層buf的寫入。


接下來,先來看一下RFC1952的部分內容:
這裏寫圖片描述
即,如果FLG.FEXTRA、FLG.FNAME、f FLG.FCOMMENT、FLG.FHCRC的值爲設置,則gzip數據中會包含相應的部分,否則不包含。這部分代碼實現如下:

if z.Extra != nil { //如果z.Extra不爲空,寫入在z.Extra;其餘同
    z.err = z.writeBytes(z.Extra)
    if z.err != nil {
        return n, z.err
    }
}
if z.Name != "" {
    z.err = z.writeString(z.Name)
    if z.err != nil {
        return n, z.err
    }
}
if z.Comment != "" {
    z.err = z.writeString(z.Comment)
    if z.err != nil {
        return n, z.err
    }
}
if z.compressor == nil {
    z.compressor, _ = flate.NewWriter(z.w, z.level)
}
z.size += uint32(len(p))
//利用循環冗餘校驗算法產生32位校驗位
z.digest = crc32.Update(z.digest, crc32.IEEETable, p)

8. Flush

該函數將所有未寫入的數據寫入下一層的Writer(io.Writer)。通過調用z.compressor.Flush()實現。

func (z *Writer) Flush() error {
    if z.err != nil {
        return z.err
    }
    if z.closed {
        return nil
    }
    if !z.wroteHeader {
        z.Write(nil)
        if z.err != nil {
            return z.err
        }
    }
    z.err = z.compressor.Flush()
    return z.err
}

9. Close

該函數把所有未寫入的數據寫入下一層Writer(io.Writer),同時爲gzip數據添加footer。

func (z *Writer) Close() error {
    if z.err != nil {
        return z.err
    }
    if z.closed {
        return nil
    }
    z.closed = true
    if !z.wroteHeader {
        z.Write(nil)
        if z.err != nil {
            return z.err
        }
    }
    //調用z.compressor.Close()實現關閉Writer
    z.err = z.compressor.Close()
    if z.err != nil {
        return z.err
    }
    //爲gzip添加footer,主要包括兩部分:32循環冗餘校驗位和數據長度
    le.PutUint32(z.buf[:4], z.digest)
    le.PutUint32(z.buf[4:8], z.size)
    _, z.err = z.w.Write(z.buf[:8])
    return z.err
}

  這就是gzip包中gzip.go文件的所有內容了,如果對函數理解的不夠清楚,還可以運行包中example_test.go中的函數,通過跟蹤函數執行過程,更清楚地瞭解這些函數。當然,如果有興趣,也可以嘗試自己閱讀分析gunzip.go文件中的內容,瞭解gzip的解碼過程。

發佈了42 篇原創文章 · 獲贊 49 · 訪問量 12萬+
發表評論
所有評論
還沒有人評論,想成為第一個評論的人麼? 請在上方評論欄輸入並且點擊發布.
相關文章