通信协议简述
- RESP(Redis Serialization ProtocolRedis)序列化协议
- 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
- 数组 以 * 号开头,后跟数组的长度。
- 将传输的结构数据分为 5 种最小单元类型,单元结束时统一加上回车换行符
客户端=>服务端
- 客户端向服务器发送的指令只有一种格式,多行字符串数组。
- 比如: 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
- 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); }
- 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 }
服务端=>客户端
-
服务器向客户端回复的响应要支持多种数据结构,所以消息响应在结构上要复杂不少。 不过也是上面5种基本类型的组合。
-
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; }
-
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;
}
}