打開瀏覽器瀏覽網頁時,我們可以看到各種各樣的文字、圖片、視頻等等各式各樣的信息。那麼瀏覽器是怎樣和服務器交互這些信息的呢?通過分析不難發現,這些信息往往要經過統一編碼之後,才進行傳遞。今天,我們來通過分析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的解碼過程。