golang標準庫對io的抽象非常精巧,各個組件可以隨意組合空繁,可以作為接口設計的典范。這篇文章結合一個實際的例子來和大家分享一下朱庆。
背景
以一個RPC的協(xié)議包來說家厌,每個包有如下結構
type Packet struct {
TotalSize uint32
Magic [4]byte
Payload []byte
Checksum uint32
}
其中TotalSize
是整個包除去TotalSize后的字節(jié)數, Magic
是一個固定長度的字串椎工,Payload
是包的實際內容饭于,包含業(yè)務邏輯的數據。
Checksum
是對Magic
和Payload
的adler32
校驗和维蒙。
編碼(encode)
我們使用一個原型為func EncodePacket(w io.Writer, payload []byte) error
的函數來把數據打包掰吕,結合encoding/binary我們很容易寫出第一版,演示需要颅痊,錯誤處理方面就簡化處理了殖熟。
var RPC_MAGIC = [4]byte{'p', 'y', 'x', 'i'}
func EncodePacket(w io.Writer, payload []byte) error {
// len(Magic) + len(Checksum) == 8
totalsize := uint32(len(payload) + 8)
// write total size
binary.Write(w, binary.BigEndian, totalsize)
// write magic bytes
binary.Write(w, binary.BigEndian, RPC_MAGIC)
// write payload
w.Write(payload)
// calculate checksum
var buf bytes.Buffer
buf.Write(RPC_MAGIC[:])
buf.Write(payload)
checksum := adler32.Checksum(buf.Bytes())
// write checksum
return binary.Write(w, binary.BigEndian, checksum)
}
在上面的實現(xiàn)中,為了計算checksum斑响,我們使用了一個內存buffer來緩存數據菱属,最后把所有的數據一次性讀出來算checksum钳榨,考慮到計算checksum是一個不斷update地過程,我們應該有方法直接略過內存buffer而計算checksum纽门。
查看hash/adler32我們得知薛耻,我們可以構造一個Hash32的對象,這個對象內嵌了一個Hash的接口赏陵,這個接口的定義如下:
type Hash interface {
// Write (via the embedded io.Writer interface) adds more data to the running hash.
// It never returns an error.
io.Writer
// Sum appends the current hash to b and returns the resulting slice.
// It does not change the underlying hash state.
Sum(b []byte) []byte
// Reset resets the Hash to its initial state.
Reset()
// Size returns the number of bytes Sum will return.
Size() int
// BlockSize returns the hash's underlying block size.
// The Write method must be able to accept any amount
// of data, but it may operate more efficiently if all writes
// are a multiple of the block size.
BlockSize() int
}
這是一個通用的計算hash的接口饼齿,標準庫里面所有計算hash的對象都實現(xiàn)了這個接口,比如md5, crc32等蝙搔。由于Hash
實現(xiàn)了io.Writer
接口缕溉,因此我們可以把所有要計算的數據像寫入文件一樣寫入到這個對象中,最后調用Sum(nil)
就可以得到最終的hash的byte數組吃型。利用這個思路证鸥,第二版可以這樣寫:
func EncodePacket2(w io.Writer, payload []byte) error {
// len(Magic) + len(Checksum) == 8
totalsize := uint32(len(RPC_MAGIC) + len(payload) + 4)
// write total size
binary.Write(w, binary.BigEndian, totalsize)
// write magic bytes
binary.Write(w, binary.BigEndian, RPC_MAGIC)
// write payload
w.Write(payload)
// calculate checksum
sum := adler32.New()
sum.Write(RPC_MAGIC[:])
sum.Write(payload)
checksum := sum.Sum32()
// write checksum
return binary.Write(w, binary.BigEndian, checksum)
}
注意這次的變化,前面寫入TotalSize勤晚,Magic枉层,Payload部分沒有變化,在計算checksum的時候去掉了bytes.Buffer运翼,減少了一次內存申請和拷貝返干。
考慮到sum
和w
都是io.Writer
,利用神奇的io.MultiWriter血淌,我們可以這樣寫
func EncodePacket(w io.Writer, payload []byte) error {
// len(Magic) + len(Checksum) == 8
totalsize := uint32(len(RPC_MAGIC) + len(payload) + 4)
// write total size
binary.Write(w, binary.BigEndian, totalsize)
sum := adler32.New()
ww := io.MultiWriter(sum, w)
// write magic bytes
binary.Write(ww, binary.BigEndian, RPC_MAGIC)
// write payload
ww.Write(payload)
// calculate checksum
checksum := sum.Sum32()
// write checksum
return binary.Write(w, binary.BigEndian, checksum)
}
注意MultiWriter的使用矩欠,我們把w
和sum
利用MultiWriter綁在了一起創(chuàng)建了一個新的Writer,向這個Writer里面寫入數據就同時向w
和sum
里面都寫入數據悠夯,這樣就完成了發(fā)送數據和計算checksum的同步進行癌淮,而對于binary.Write
來說沒有任何區(qū)別,因為它需要的是一個實現(xiàn)了Write
方法的對象沦补。
解碼(decode)
基于上面的思想乳蓄,解碼也可以把接收數據和計算checksum一起進行,完整代碼如下
func DecodePacket(r io.Reader) ([]byte, error) {
var totalsize uint32
err := binary.Read(r, binary.BigEndian, &totalsize)
if err != nil {
return nil, errors.Annotate(err, "read total size")
}
// at least len(magic) + len(checksum)
if totalsize < 8 {
return nil, errors.Errorf("bad packet. header:%d", totalsize)
}
sum := adler32.New()
rr := io.TeeReader(r, sum)
var magic [4]byte
err = binary.Read(rr, binary.BigEndian, &magic)
if err != nil {
return nil, errors.Annotate(err, "read magic")
}
if magic != RPC_MAGIC {
return nil, errors.Errorf("bad rpc magic:%v", magic)
}
payload := make([]byte, totalsize-8)
_, err = io.ReadFull(rr, payload)
if err != nil {
return nil, errors.Annotate(err, "read payload")
}
var checksum uint32
err = binary.Read(r, binary.BigEndian, &checksum)
if err != nil {
return nil, errors.Annotate(err, "read checksum")
}
if checksum != sum.Sum32() {
return nil, errors.Errorf("checkSum error, %d(calc) %d(remote)", sum.Sum32(), checksum)
}
return payload, nil
}
上面代碼中夕膀,我們使用了io.TeeReader虚倒,這個函數的原型為func TeeReader(r Reader, w Writer) Reader
,它返回一個Reader产舞,這個Reader是參數r
的代理魂奥,讀取的數據還是來自r
,不過同時把讀取的數據寫入到w
里面易猫。
一切皆文件
unix下有一切皆文件的思想耻煤,golang把這個思想貫徹到更遠,因為本質上我們對文件的抽象就是一個可讀可寫的一個對象,也就是實現(xiàn)了io.Writer
和io.Reader
的對象我們都可以稱為文件哈蝇,在上面的例子中無論是EncodePacket
還是DecodePacket
我們都沒有假定編碼后的數據是發(fā)送到socket棺妓,還是從內存讀取數據解碼,因此我們可以這樣調用EncodePacket
conn, _ := net.Dial("tcp", "127.0.0.1:8000")
EncodePacket(conn, []byte("hello"))
把數據直接發(fā)送到socket炮赦,也可以這樣
conn, _ := net.Dial("tcp", "127.0.0.1:8000")
bufconn := bufio.NewWriter(conn)
EncodePacket(bufconn, []byte("hello"))
對socket加上一個buffer來增加吞吐量怜跑,也可以這樣
conn, _ := net.Dial("tcp", "127.0.0.1:8000")
zip := zlib.NewWriter(conn)
bufconn := bufio.NewWriter(conn)
EncodePacket(bufconn, []byte("hello"))
加上一個zip壓縮,還可以利用加上crypto/aes來個AES加密...
在這個時候眼五,文件已經不再局限于io妆艘,可以是一個內存buffer彤灶,也可以是一個計算hash的對象看幼,甚至是一個計數器,流量限速器幌陕。golang靈活的接口機制為我們提供了無限可能诵姜。
結尾
我一直認為一個好的語言一定有一個設計良好的標準庫,golang的標準庫是作者們多年系統(tǒng)編程的沉淀搏熄,值得我們細細品味