用Go實現Redis之四實現Redis的協議交互

寫在前面

本文實現的Godis代碼版本爲:v0.0.3git

在前三篇文章中,實現了客戶端/服務端的交互(基於textprotoco)、服務端初始化和get/set命令。若是閱讀過或者調試過粗略的代碼實現,會發現使用文本協議進行交互,除了容易閱讀以外,解析效率是比較低下的。
由於咱們的示例是"set alpha 123n",工整的單個空格和n分割,可能在分割上效率還好;既要分割,難免低效。github

在本文,將替換文本協議爲Redis1.2版本後的統一協議。redis

Redis通訊協議

Redis通訊協議解析高效、二進制安全,同時也對人類友好(可直接閱讀解析)。segmentfault

協議格式

Redis在發送命令和返回結果中均使用同一套標準協議。Reids協議「肉眼可辨」,在發送命令是使用類型爲"multi bulk reply"的協議類型,回覆時根據結果的不一樣使用不一樣類型協議。數組

經過檢查服務器發回數據的第一個字節, 能夠肯定這個回覆是什麼類型:安全

  1. 狀態回覆(status reply)的第一個字節是 "+"
  2. 錯誤回覆(error reply)的第一個字節是 "-"
  3. 整數回覆(integer reply)的第一個字節是 ":"
  4. 批量回復(bulk reply)的第一個字節是 "$"
  5. 多條批量回復(multi bulk reply)的第一個字節是 "*"

舉兩個例子:

1.客戶端執行命令"set alpha 123", 服務器返回 "OK"
該類型即爲狀態恢復,服務器返回的結果封裝爲標準協議是"+OKrn",客戶端解釋協議結果,將之反饋給使用者。服務器

2.仍是客戶端執行命令"set alpha 123",在發送給服務端時也是以協議格式交互的。前文提到發送命令使用的是」多條批量回復「類型協議,封裝好的命令就是*3\r\n$3\r\nset\r\n$5\r\nalpha\r\n$3\r\n123\r\n
對應的ASCII碼以下:app

clipboard.png

  1. 符號'*'標識協議類型是多條批量回復,"rn"爲元素分割標記;
  2. '$'標識接下來的是批量回復協議,要按照批量回復格式解析;
  3. '3'表明該批量回復長度爲3字節;
  4. "set"爲批量回復協議內容;
  5. 重複2-4直到協議解析完成。

能夠看出,協議的生成和解析能夠簡化理解爲兩段文本處理程序。工具

Godis實現Redis通訊協議

GO版本協議實現初探

不少Redis相關的GO組件、模塊、工具都有協議的生成和解析實現,並歷經生產環境的考驗。如go-rediscodis等知名項目。
不提性能和擴展性,協議生成的GO代碼能夠實現以下:性能

//將命令行轉換爲協議
func Cmd2Protocol(cmd string) (pro string) {
    //cmd := "set alpha 123"
    ret := strings.Split(cmd, " ")
    //todo validate cmd and params
    for k, v := range ret {
        if k == 0 {
            pro = fmt.Sprintf("*%d\r\n", len(ret))
        }
        pro += fmt.Sprintf("$%d\r\n%s\r\n", len(v), v)
    }
    return
}

以上代碼即可以將命令"set alpha 123"轉換爲Redis的標準協議格式。

而協議的解析,能夠拆解爲以下流程:

clipboard.png

之前文示例,拆解過程以下:

clipboard.png

最終的操做只是單獨的數據類型解析,數字解析將數字轉成文字、文本解析讀取對應字節數量的字符便可。

//將協議轉成argc、argv
func Protocol2Args(protocol string) (argv []string, argc int) {
    parts := strings.Split(strings.Trim(protocol, " "), "\r\n")
    if len(parts) == 0 {
        return nil, 0
    }
    argc, err := strconv.Atoi(parts[0][1:])
    if err != nil {
        return nil, 0
    }
    j := 0
    var vlen []int
    for _, v := range parts[1:] {
        if len(v) == 0 {
            continue
        }
        if v[0] == '$' {
            tmpl, err := strconv.Atoi(v[1:])
            if err == nil {
                vlen = append(vlen, tmpl)
            }
        } else {
            if j < len(vlen) && vlen[j] == len(v) {
                j++
                argv = append(argv, v)
            }
        }
    }
    return argv, argc
}

協議最終實現

在實現協議的編碼過程當中,一直但願編碼能儘量簡單、又有值得思考和改進的地方,無奈能力有限,遠不如codis的實現優雅。仍是以爲使用codis的實現方案,纔是值得一看的代碼。對codis的代碼作了部分修改,若是想直接看codis的實現,能夠點這裏直達。
在Godis的協議實現中,去掉了codis的錯誤處理和一部分I/O優化,但願儘可能讓其看起來簡單,但願不會生硬:)。
主要增長了兩個包:
其一爲共用的帶緩衝I/O包,封裝了ByteReader的一些byte級操做
其二爲proto包,分別可實例化爲proto.Encoder和proto.Decoder來處理協議編解碼

協議編碼

將release v0.0.2中的純文本協議交互改成編碼後的協議交互:

func send2Server(msg string, conn net.Conn) (n int, err error) {
    p, e := proto.EncodeCmd(msg)
    if e != nil {
        return 0, e
    }
    //fmt.Println("proto encode", p, string(p))
    n, err = conn.Write(p)
    return n, err
}

前文說過,編碼使用的協議類型是多條批量回復。這裏仍然以"set alpha 123"命令爲例。
首先,拆解字符串爲[set alpha 123]三部分(請暫時忽略異常格式)。三部分分別是一條批量回復,每一部分按照一個批量回復格式編碼處理便可。
在proto包,使用以下結構體保存協議格式和數據信息:

type Resp struct {
    Type byte

    Value []byte
    Array []*Resp
}

以上文例子,單條批量回復"set",填充進Resp結構的方法是:

// NewBulkBytes 批量回復類型
func NewBulkBytes(value []byte) *Resp {
    r := &Resp{}
    r.Type = TypeBulkBytes//批量回復類型
    r.Value = value
    return r
}

"set","alpha","123"三條批量回復構成多條批量回復類型的方法以下:

// NewArray 多條批量回復類型
func NewArray(array []*Resp) *Resp {
    r := &Resp{}
    r.Type = TypeArray//多條批量回復
    r.Array = array
    return r
}

這樣就將[set alpha 123]構成了多條批量回復類型的協議。而在將該多條批量回復類型的協議編碼的操做僞代碼以下:

// encodeResp 編碼
func (e *Encoder) encodeResp(r *Resp) error {
    if err := e.bw.WriteByte(byte(r.Type)); err != nil {
        return errorsTrace(err)
    }
    switch r.Type {
    case TypeString, TypeError, TypeInt:
        return e.encodeTextBytes(r.Value)
    case TypeBulkBytes:
        return e.encodeBulkBytes(r.Value)
    case TypeArray:
        return e.encodeArray(r.Array)
    default:
        return errorsTrace(e.Err)
    }
}
// encodeArray encode 多條批量回復
func (e *Encoder) encodeArray(array []*Resp) error {
    if array == nil {
        return e.encodeInt(-1)
    } else {
        if err := e.encodeInt(int64(len(array))); err != nil {
            return err
        }
        for _, r := range array {
            if err := e.encodeResp(r); err != nil {
                return err
            }
        }
        return nil
    }
}

——編碼多條批量回復的操做是先逐條編碼Resp.Array數組的元素,好比"set",真正的編碼操做爲將"set"長度、分隔符"rn"和"set"自己分別追加到協議,
結果就是$3\r\nset\r\n

協議解碼

協議生成的過程只依賴多條批量回復類型,而客戶端在解讀服務端的返回時,會面臨不一樣的回覆類型:

// decodeResp 根據返回類型調用不一樣解析實現
func (d *Decoder) decodeResp() (*Resp, error) {
    b, err := d.br.ReadByte()
    if err != nil {
        return nil, errorsTrace(err)
    }
    r := &Resp{}
    r.Type = byte(b)
    switch r.Type {
    default:
        return nil, errorsTrace(err)
    case TypeString, TypeError, TypeInt:
        r.Value, err = d.decodeTextBytes()
    case TypeBulkBytes:
        r.Value, err = d.decodeBulkBytes()
    case TypeArray:
        r.Array, err = d.decodeArray()
    }
    return r, err
}

該過程與編碼過程操做相似,再也不贅述。下面的代碼是爲服務端增長協議解析:

// ProcessInputBuffer 處理客戶端請求信息
func (c *Client) ProcessInputBuffer() error {
    //r := regexp.MustCompile("[^\\s]+")
    decoder := proto.NewDecoder(bytes.NewReader([]byte(c.QueryBuf)))
    //decoder := proto.NewDecoder(bytes.NewReader([]byte("*2\r\n$3\r\nget\r\n")))
    if resp, err := decoder.DecodeMultiBulk(); err == nil {
        c.Argc = len(resp)
        c.Argv = make([]*GodisObject, c.Argc)
        for k, s := range resp {
            c.Argv[k] = CreateObject(ObjectTypeString, string(s.Value))
        }
        return nil
    }
    return errors.New("ProcessInputBuffer failed")
}

這裏是一些調試信息:

clipboard.png

最後請看添加了協議實現以後的演示:

clipboard.png

由於都是通過客戶端/服務端的編解碼以後的結果,並不能看出協議自己的內容。感興趣的讀者能夠直接編譯本篇的release版本v0.0.3,打開調試日誌查看交互過程的協議實現。

本篇問題

  1. bufio包的實現中,涉及到一些GO版本和讀寫操做的問題,細節不容易講清楚;
  2. 單獨編寫的Encoder和Decoder在實現上有一些效率和擴展性問題,歡迎討論。

下集預告

  1. AOF持久化——數據保存;
  2. AOF持久化——啓動加載。
相關文章
相關標籤/搜索