用 Go 來了解一下 Redis 通信協議

原文地址:用 Go 來了解一下 Redis 通信協議html

Go、PHP、Java... 都有那麼多包來支撐你使用 Redis,那你是否有想過git

有了服務端,有了客戶端,他們倆是怎樣通信,又是基於什麼通信協議作出交互的呢?github

介紹

基於咱們的目的,本文主要講解和實踐 Redis 的通信協議golang

Redis 的客戶端和服務端是經過 TCP 鏈接來進行數據交互, 服務器默認的端口號爲 6379redis

客戶端和服務器發送的命令或數據一概以 \r\n(CRLF)結尾(這是一條約定)安全

協議

在 Redis 中分爲請求回覆,而請求協議又分爲新版和舊版,新版統一請求協議在 Redis 1.2 版本中引入,最終在 Redis 2.0 版本成爲 Redis 服務器通訊的標準方式bash

本文是基於新版協議來實現功能,不建議使用舊版(1.2 挺老舊了)。以下是新協議的各類範例:服務器

請求協議

一、 格式示例app

*<參數數量> CR LF
$<參數 1 的字節數量> CR LF
<參數 1 的數據> CR LF
...
$<參數 N 的字節數量> CR LF
<參數 N 的數據> CR LF
複製代碼

在該協議下全部發送至 Redis 服務器的參數都是二進制安全(binary safe)的tcp

二、打印示例

*3
$3
SET
$5
mykey
$7
myvalue
複製代碼

三、實際協議值

"*3\r\n$3\r\nSET\r\n$5\r\nmykey\r\n$7\r\nmyvalue\r\n"
複製代碼

這就是 Redis 的請求協議規範,按照範例1編寫客戶端邏輯,最終發送的是範例3,相信你已經有大體的概念了,Redis 的協議很是的簡潔易懂,這也是好上手的緣由之一,你能夠想一想協議這麼定義的好處在哪?

回覆

Redis 會根據你請求協議的不一樣(執行的命令結果也不一樣),返回多種不一樣類型的回覆。在這個回覆「協議」中,能夠經過檢查第一個字節,肯定這個回覆是什麼類型,以下:

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

有了回覆的頭部標識,結尾的 CRLF,你能夠大體猜測出回覆「協議」是怎麼樣的,可是實踐才能得出真理,斎知道怕是你很快就忘記了 😀

實踐

與 Redis 服務器交互

package main

import (
	"log"
	"net"
	"os"

	"github.com/EDDYCJY/redis-protocol-example/protocol"
)

const (
	Address = "127.0.0.1:6379"
	Network = "tcp"
)

func Conn(network, address string) (net.Conn, error) {
	conn, err := net.Dial(network, address)
	if err != nil {
		return nil, err
	}

	return conn, nil
}

func main() {
        // 讀取入參
	args := os.Args[1:]
	if len(args) <= 0 {
		log.Fatalf("Os.Args <= 0")
	}
    
        // 獲取請求協議
	reqCommand := protocol.GetRequest(args)
	
	// 鏈接 Redis 服務器
	redisConn, err := Conn(Network, Address)
	if err != nil {
		log.Fatalf("Conn err: %v", err)
	}
	defer redisConn.Close()
    
        // 寫入請求內容
	_, err = redisConn.Write(reqCommand)
	if err != nil {
		log.Fatalf("Conn Write err: %v", err)
	}
    
        // 讀取回復
	command := make([]byte, 1024)
	n, err := redisConn.Read(command)
	if err != nil {
		log.Fatalf("Conn Read err: %v", err)
	}
    
        // 處理回覆
	reply, err := protocol.GetReply(command[:n])
	if err != nil {
		log.Fatalf("protocol.GetReply err: %v", err)
	}
    
        // 處理後的回覆內容
	log.Printf("Reply: %v", reply)
	// 原始的回覆內容
	log.Printf("Command: %v", string(command[:n]))
}
複製代碼

在這裏咱們完成了整個 Redis 客戶端和服務端交互的流程,分別以下:

一、讀取命令行參數:獲取執行的 Redis 命令

二、獲取請求協議參數

三、鏈接 Redis 服務器,獲取鏈接句柄

四、將請求協議參數寫入鏈接:發送請求的命令行參數

五、從鏈接中讀取返回的數據:讀取先前請求的回覆數據

六、根據回覆「協議」內容,處理回覆的數據集

七、輸出處理後的回覆內容及原始回覆內容

請求

func GetRequest(args []string) []byte {
	req := []string{
		"*" + strconv.Itoa(len(args)),
	}

	for _, arg := range args {
		req = append(req, "$"+strconv.Itoa(len(arg)))
		req = append(req, arg)
	}

	str := strings.Join(req, "\r\n")
	return []byte(str + "\r\n")
}
複製代碼

經過對 Redis 的請求協議的分析,可得出它的規律,先加上標誌位,計算參數總數量,再循環合併各個參數的字節數量、值就能夠了

回覆

func GetReply(reply []byte) (interface{}, error) {
	replyType := reply[0]
	switch replyType {
	case StatusReply:
		return doStatusReply(reply[1:])
	case ErrorReply:
		return doErrorReply(reply[1:])
	case IntegerReply:
		return doIntegerReply(reply[1:])
	case BulkReply:
		return doBulkReply(reply[1:])
	case MultiBulkReply:
		return doMultiBulkReply(reply[1:])
	default:
		return nil, nil
	}
}

func doStatusReply(reply []byte) (string, error) {
	if len(reply) == 3 && reply[1] == 'O' && reply[2] == 'K' {
		return OkReply, nil
	}

	if len(reply) == 5 && reply[1] == 'P' && reply[2] == 'O' && reply[3] == 'N' && reply[4] == 'G' {
		return PongReply, nil
	}

	return string(reply), nil
}

func doErrorReply(reply []byte) (string, error) {
	return string(reply), nil
}

func doIntegerReply(reply []byte) (int, error) {
	pos := getFlagPos('\r', reply)
	result, err := strconv.Atoi(string(reply[:pos]))
	if err != nil {
		return 0, err
	}

	return result, nil
}

...
複製代碼

在這裏咱們對全部回覆類型進行了分發,不一樣的回覆標誌位對應不一樣的處理方式,在這裏需求注意幾項問題,以下:

一、當請求的值不存在,會將特殊值 -1 用做回覆

二、服務器發送的全部字符串都由 CRLF 結尾

三、多條批量回復是可基於批量回復的,要注意理解

四、無內容的多條批量回復是存在的

最重要的是,對不一樣回覆的規則的把控,可以讓你更好的理解 Redis 的請求、回覆的交互過程 👌

小結

寫這篇文章的原由,是由於經常在使用 Redis 時,只是用,你不知道它是基於什麼樣的通信協議來通信,這樣的感受是十分難受的

經過本文的講解,我相信你已經大體瞭解 Redis 客戶端是怎麼樣和服務端交互,也清楚了其所用的通信原理,但願可以對你有所幫助!

最後,若是想詳細查看代碼,右拐 項目地址

若是對你有所幫助,歡迎點個 Star 👍

參考

相關文章
相關標籤/搜索