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

來源:互聯網
上載者:User

寫在前面

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

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

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

Redis通訊協定

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

協議格式

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碼如下:

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

可以看出,協議的產生和解析可以簡化理解為兩段文本處理常式。

Godis實現Redis通訊協定

GO版本協議實現初探

很多Redis相關的GO組件、模組、工具都有協議的產生和解析實現,並曆經生產環境的考驗。如go-redis、codis等知名項目。
不提效能和擴充性,協議產生的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的標準協議格式。

而協議的解析,可以拆解為如下流程:

以前文樣本,拆解過程如下:

最終的操作只是單獨的資料類型解析,數字解析將數字轉成文字、文本解析讀取對應位元組數量的字元即可。

//將協議轉成argc、argvfunc 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")}

這裡是一些調試資訊:

最後請看添加了協議實現之後的示範:

因為都是經過用戶端/服務端的編解碼之後的結果,並不能看出協議本身的內容。感興趣的讀者可以直接編譯本篇的release版本v0.0.3,開啟調試日誌查看互動過程的協議實現。

本篇問題

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

下集預告

  1. AOF持久化實現
相關文章

聯繫我們

該頁面正文內容均來源於網絡整理,並不代表阿里雲官方的觀點,該頁面所提到的產品和服務也與阿里云無關,如果該頁面內容對您造成了困擾,歡迎寫郵件給我們,收到郵件我們將在5個工作日內處理。

如果您發現本社區中有涉嫌抄襲的內容,歡迎發送郵件至: info-contact@alibabacloud.com 進行舉報並提供相關證據,工作人員會在 5 個工作天內聯絡您,一經查實,本站將立刻刪除涉嫌侵權內容。

A Free Trial That Lets You Build Big!

Start building with 50+ products and up to 12 months usage for Elastic Compute Service

  • Sales Support

    1 on 1 presale consultation

  • After-Sales Support

    24/7 Technical Support 6 Free Tickets per Quarter Faster Response

  • Alibaba Cloud offers highly flexible support services tailored to meet your exact needs.