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;
    }
}
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值