通信協議簡述
- 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;
}
}