Redis通信協議——RESP

通信協議簡述

  1. RESP(Redis Serialization ProtocolRedis)序列化協議
  2. Redis 協議規則:
    • 將傳輸的結構數據分爲 5 種最小單元類型,單元結束時統一加上回車換行符
      號\r\n。
    • 單行字符串 以 + 符號開頭。(也可用多行的形式,實現統一)
    • 多行字符串 以 $ 符號開頭,後跟字符串長度。

      NULL長度爲-1 : $-1\r\n
      空串長度填 0: $0\r\n\r\n

    • 整數值以 : 符號開頭,後跟整數的字符串形式。

      比如成功返回1 表示 :1

    • 錯誤消息 以 - 符號開頭。

      錯誤信息返回表示 -ERR value is not an integer or out of range

    • 數組 以 * 號開頭,後跟數組的長度。

客戶端=>服務端

  1. 客戶端向服務器發送的指令只有一種格式,多行字符串數組。
  2. 比如: set hello world 會序列化爲:
     *3 \r\n $3 \r\n set \r\n $5 \r\n hello \r\n $5 \r\n world \r\n
     # 多行字符串數組 : * 開始 帶 \r\n
     # 後面開始帶每個的字符串 set :  $3\r\nset   等等 
     # 控制檯輸出該序列化後的爲:
         *3
    	 $3
         set
         $5
         hello
         $5
         world
    
  3. php實現
    protected function _makeCommand($args)
        {
            $cmds = array();
            $cmds[] = '*' . count($args) . "\r\n";
            foreach($args as $arg)
            {
                $cmds[] = '$' . strlen($arg) . "\r\n$arg\r\n";
            }
            $this->command = implode($cmds);
        }
    
  4. go實現: 參考"github.com/garyburd/redigo/redis"
    	func (c *conn) writeCommand(cmd string, args []interface{}) error {
    		c.writeLen('*', 1+len(args))
    		if err := c.writeString(cmd); err != nil {
    			return err
    		}
    		for _, arg := range args {
    			if err := c.writeArg(arg, true); err != nil {
    				return err
    			}
    		}
    		return nil
    	}
    	
    	func (c *conn) writeString(s string) error {
    		c.writeLen('$', len(s))
    		c.bw.WriteString(s)
    		_, err := c.bw.WriteString("\r\n")
    		return err
    	}
    

服務端=>客戶端

  1. 服務器向客戶端回覆的響應要支持多種數據結構,所以消息響應在結構上要複雜不少。 不過也是上面5種基本類型的組合。

  2. php實現

        protected function _fmtResult()
        {
            if ($this->response[0] == '-')
            {
                $this->response = ltrim($this->response, '-');
                list($errstr, $this->response) = explode("\r\n", $this->response, 2);
                throw new PhpRedisException($errstr, 500);
            }
            switch($this->response[0])
            {
                case '+':
                case ':':
                    list($ret, $this->response) = explode("\r\n", $this->response, 2);
                    $ret = substr($ret, 1);
                    break;
                case '$':
                    $this->response = ltrim($this->response, '$');
                    list($slen, $this->response) = explode("\r\n", $this->response, 2);
                    $ret = substr($this->response, 0, intval($slen));
                    $this->response = substr($this->response, 2 + $slen);
                    break;
                case '*':
                    $ret = $this->_resToArray();
                    break;
            }
            return $ret;
        }
    
  3. go實現

    func (c *conn) readReply() (interface{}, error) {
    	line, err := c.readLine()
    	if err != nil {
    		return nil, err
    	}
    	if len(line) == 0 {
    		return nil, protocolError("short response line")
    	}
    	switch line[0] {
    		case '+':
    			switch {
    			case len(line) == 3 && line[1] == 'O' && line[2] == 'K':
    				// Avoid allocation for frequent "+OK" response.
    				return okReply, nil
    			case len(line) == 5 && line[1] == 'P' && line[2] == 'O' && line[3] == 'N' && line[4] == 'G':
    				// Avoid allocation in PING command benchmarks :)
    				return pongReply, nil
    			default:
    				return string(line[1:]), nil
    			}
    		case '-':
    			return Error(string(line[1:])), nil
    		case ':':
    			return parseInt(line[1:])
    		case '$':
    			n, err := parseLen(line[1:])
    			if n < 0 || err != nil {
    				return nil, err
    			}
    			p := make([]byte, n)
    			_, err = io.ReadFull(c.br, p)
    			if err != nil {
    				return nil, err
    			}
    			if line, err := c.readLine(); err != nil {
    				return nil, err
    			} else if len(line) != 0 {
    				return nil, protocolError("bad bulk string format")
    			}
    			return p, nil
    		case '*':
    			n, err := parseLen(line[1:])
    			if n < 0 || err != nil {
    				return nil, err
    			}
    			r := make([]interface{}, n)
    			for i := range r {
    				r[i], err = c.readReply()
    				if err != nil {
    					return nil, err
    				}
    			}
    		return r, nil
    	}
    	return nil, protocolError("unexpected response line")
    }
    

實現Redis和客戶端RESP協議通信的WEB端:

代碼倉庫: PRedis

<?php
/**
 * Created by PhpStorm.
 * User: shuxnhs
 * Date: 18-11-16
 * Time: 下午10:27
 */
class PhpRedisException extends Exception{}
class PhpRedis
{
    protected $conn = NULL;
    protected $command = NULL;
    protected $isPipeline = FALSE;
    protected $pipelineCmd = '';
    protected $pipelineCount = 0;
    protected $response = '';
    public function connect($host = '127.0.0.1', $port = 6379, $timeout = 0)
    {
        $this->conn = stream_socket_client("tcp://$host:$port", $errno, $errstr, $timeout);
        if (!$this->conn)
        {
            throw new PhpRedisException("無法連接redis服務器:$errstr", $errno);
        }
        return $this->conn;
    }
    public function checkConnect($chost,$cport, $timeout = 0){
        $this->conn = stream_socket_client("tcp://$chost:$cport", $errno, $errstr, $timeout);
        if (!$this->conn)
        {
            throw new PhpRedisException("無法連接redis服務器:$errstr", $errno);
        }
        return $this->conn;
    }
    protected function _makeCommand($args)
    {
        $cmds = array();
        $cmds[] = '*' . count($args) . "\r\n";
        foreach($args as $arg)
        {
            $cmds[] = '$' . strlen($arg) . "\r\n$arg\r\n";
        }
        $this->command = implode($cmds);
    }
    protected function _fmtResult()
    {
        if ($this->response[0] == '-')
        {
            $this->response = ltrim($this->response, '-');
            list($errstr, $this->response) = explode("\r\n", $this->response, 2);
            throw new PhpRedisException($errstr, 500);
        }
        switch($this->response[0])
        {
            case '+':
            case ':':
                list($ret, $this->response) = explode("\r\n", $this->response, 2);
                $ret = substr($ret, 1);
                break;
            case '$':
                $this->response = ltrim($this->response, '$');
                list($slen, $this->response) = explode("\r\n", $this->response, 2);
                $ret = substr($this->response, 0, intval($slen));
                $this->response = substr($this->response, 2 + $slen);
                break;
            case '*':
                $ret = $this->_resToArray();
                break;
        }
        return $ret;
    }
    protected function _resToArray()
    {
        $ret = array();
        $this->response = ltrim($this->response, '*');
        list($count, $this->response) = explode("\r\n", $this->response, 2);
        for($i = 0; $i < $count; $i++)
        {
            $tmp = $this->_fmtResult();
            $ret[] = $tmp;
        }
        return $ret;
    }
    protected function _fetchResponse()
    {
        $this->response = fread($this->conn, 8196);
        stream_set_blocking($this->conn, 0); // 設置連接爲非阻塞
        // 繼續讀取返回結果
        while($buf = fread($this->conn, 8196))
        {
            $this->response .= $buf;
        }
        stream_set_blocking($this->conn, 1); // 恢復連接爲阻塞
    }
    public function exec()
    {
        if (func_num_args() == 0)
        {
            throw new PhpRedisException("參數不可以爲空", 301);
        }
        $this->_makeCommand(func_get_args());
        if (TRUE === $this->isPipeline)
        {
            $this->pipelineCmd .= $this->command;
            $this->pipelineCount++;
            return;
        }
        //echo $this->command;
        fwrite($this->conn, $this->command, strlen($this->command));
        $this->_fetchResponse();
        //echo $this->response;
        return $this->_fmtResult();
    }
    public function initPipeline()
    {
        $this->isPipeline = TRUE;
        $this->pipelineCount = 0;
        $this->pipelineCmd = '';
    }
    public function commitPipeline()
    {
        $ret = array();
        if ($this->pipelineCmd)
        {
            fwrite($this->conn, $this->pipelineCmd, strlen($this->pipelineCmd));
            $this->_fetchResponse();
            for($i = 0; $i < $this->pipelineCount; $i++)
            {
                $ret[] = $this->_fmtResult();
            }
        }
        $this->isPipeline = FALSE;
        $this->pipelineCmd = '';
        return $ret;
    }
    public function close()
    {
        @stream_socket_shutdown($this->conn, STREAM_SHUT_RDWR);
        @fclose($this->conn);
        $this->conn = NULL;
    }
}
發佈了92 篇原創文章 · 獲贊 40 · 訪問量 4萬+
發表評論
所有評論
還沒有人評論,想成為第一個評論的人麼? 請在上方評論欄輸入並且點擊發布.
相關文章