摘要:在本文,將替換文本協議為版本后的統一協議。協議格式在發送命令和返回結果中均使用同一套標準協議。實現通信協議版本協議實現初探很多相關的組件模塊工具都有協議的生成和解析實現,并歷經生產環境的考驗。
寫在前面
本文實現的Godis代碼版本為:v0.0.3
在前三篇文章中,實現了客戶端/服務端的交互(基于textprotoco)、服務端初始化和get/set命令。如果閱讀過或者調試過粗略的代碼實現,會發現使用文本協議進行交互,除了容易閱讀之外,解析效率是比較低下的。
因為我們的示例是"set alpha 123n",工整的單個空格和n分割,可能在分割上效率還好;既要分割,不免低效。
在本文,將替換文本協議為Redis1.2版本后的統一協議。
Redis通信協議Redis通信協議解析高效、二進制安全,同時也對人類友好(可直接閱讀解析)。
協議格式Redis在發送命令和返回結果中均使用同一套標準協議。Reids協議“肉眼可辨”,在發送命令是使用類型為"multi bulk reply"的協議類型,回復時根據結果的不同使用不同類型協議。
通過檢查服務器發回數據的第一個字節, 可以確定這個回復是什么類型:
狀態回復(status reply)的第一個字節是 "+"
錯誤回復(error reply)的第一個字節是 "-"
整數回復(integer reply)的第一個字節是 ":"
批量回復(bulk reply)的第一個字節是 "$"
多條批量回復(multi bulk reply)的第一個字節是 "*"
舉兩個例子:1.客戶端執行命令"set alpha 123", 服務器返回 "OK"
該類型即為狀態恢復,服務器返回的結果封裝為標準協議是"+OKrn",客戶端解釋協議結果,將之反饋給使用者。
2.還是客戶端執行命令"set alpha 123",在發送給服務端時也是以協議格式交互的。前文提到發送命令使用的是”多條批量回復“類型協議,封裝好的命令就是*3
$3
set
$5
alpha
$3
123
對應的ASCII碼如下:
符號"*"標識協議類型是多條批量回復,"rn"為元素分割標記;
"$"標識接下來的是批量回復協議,要按照批量回復格式解析;
"3"代表該批量回復長度為3字節;
"set"為批量回復協議內容;
重復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 ", len(ret)) } pro += fmt.Sprintf("$%d %s ", len(v), v) } return }
以上代碼便可以將命令"set alpha 123"轉換為Redis的標準協議格式。
而協議的解析,可以拆解為如下流程:
以前文示例,拆解過程如下:
最終的操作只是多帶帶的數據類型解析,數字解析將數字轉成文字、文本解析讀取對應字節數量的字符即可。
//將協議轉成argc、argv func Protocol2Args(protocol string) (argv []string, argc int) { parts := strings.Split(strings.Trim(protocol, " "), " ") 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
set
協議生成的過程只依賴多條批量回復類型,而客戶端在解讀服務端的返回時,會面臨不同的回復類型:
// 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 $3 get "))) 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,打開調試日志查看交互過程的協議實現。
本篇問題bufio包的實現中,涉及到一些GO版本和讀寫操作的問題,細節不容易講清楚;
多帶帶編寫的Encoder和Decoder在實現上有一些效率和擴展性問題,歡迎討論。
下集預告AOF持久化——數據保存;
AOF持久化——啟動加載。
文章版權歸作者所有,未經允許請勿轉載,若此文章存在違規行為,您可以聯系管理員刪除。
轉載請注明本文地址:http://specialneedsforspecialkids.com/yun/28852.html
摘要:用來了解一下通訊協議原文地址用來了解一下通訊協議都有那么多包來支撐你使用,那你是否有想過有了服務端,有了客戶端,他們倆是怎樣通訊,又是基于什么通訊協議做出交互的呢介紹基于我們的目的,本文主要講解和實踐的通訊協議的客戶端和服務端是通過連接來進 用 Go 來了解一下 Redis 通訊協議 原文地址:用 Go 來了解一下 Redis 通訊協議 Go、PHP、Java... 都有那么多包來支...
摘要:命令實現命令是最常用的命令之一,也是最能反映緩存發展歷史的操作。命令在客戶端接收之后,經由協議轉換傳遞給服務端執行。服務端執行命令前先查詢是否支持該命令,以決定是否執行。,是的簡稱,代表的是只存增量的持久化方式。 緣起 最近公司的第一個PHP轉GO項目已經在生產環境穩定運行數周,又逢需求小年兒,最近可以得空分享下去年學GO過程中的練手項目Godis——用Golang實現的Redis. ...
閱讀 3859·2023-04-26 00:36
閱讀 2667·2021-11-16 11:44
閱讀 1082·2021-11-15 17:58
閱讀 1665·2021-09-30 09:47
閱讀 1208·2019-08-30 13:05
閱讀 1539·2019-08-30 12:55
閱讀 2409·2019-08-30 11:02
閱讀 2718·2019-08-29 17:01