- redis通信协议
redis通信协议由tcp协议进行数据交互,默认端口为6379
请求
Redis 服务器接受命令以及命令的参数。
服务器会在接到命令之后,对命令进行处理,并将命令的回复传送回客户端。
命令格式为:
*<参数数量> CRLF $<参数 1 的字节数量> CRLF <参数 1 的数据> CRLF ... $<参数 N 的字节数量> CRLF <参数 N 的数据> CRLF
命令本身也作为协议的其中一个参数来发送。
例如:
set a tioncico
tcp原始数据打印为:
string(34) "*3 $3 set $1 a $8 tioncico "
实际数据为:
string(34) "*3CRLF$3CRLFsetCRLF$1CRLFa$8CRLFtioncicoCRLF"
CRLF也就是"\r\n"
redis 响应
当redis服务器接收到请求时,会做出响应,redis会根据不同的命令以及数据,返回不同类型的数据
redis响应类型
通过检查redis服务器返回数据的第一个字节,可确定这个回复是什么类型:
1:"+" 状态回复(status reply) 2:"-" 错误回复(error reply) 3:":" 整数回复(integer reply) 4:"$" 批量回复(bulk reply) 5:"*" 多条批量回复(multi bulk reply)
状态回复
一个状态回复(或者单行回复,single line reply)是一段以 “+” 开始、 “\r\n” 结尾的单行字符串。
例如:
当你set a tioncic之后,redis服务器会给你回复:
+OK\\r\\n
错误回复
错误回复第一个字节以"-"开头:
示例:
当你 st a tioncico 发送不存在的"st"命令时:
-ERR unknown command 'st'\\r\\n
在 “-” 之后,直到遇到第一个空格或新行为止,这中间的内容表示所返回错误的类型。剩余内容为错误内容
除了ERR错误这种通用型错误之外,还有更加特定的错误消息,例如:
-WRONGTYPE Operation against a key holding the wrong kind of value
整数回复
由":"开头,\r\n结尾的消息为整数回复,例如:
:1000\\r\\n
":"到\r\n中间的内容即是整数回复
返回值的唯一限制是该数据必须用64位整数存储
批量回复
服务器使用批量回复来返回二进制安全的字符串,字符串的最大长度为 512 MB 。
例如:
get a (在上面的例子中,已经set a的值为tioncico)
将返回
$8\\r\\ntioncico\\r\\n
服务器发送的内容格式为:
1:第一个字符为"$" 2:随后跟着随机回复内容的长度值8 3:然后跟着CRLF(\\r\\n) 4:然后随后的8个字节,都代表是回复的内容 5:然后再跟着CRLF(\\r\\n) 6:该回复完成
特殊情况
1:当你的数据本身带\r\n时,无需顾虑,直接跟着回复内容的长度值进行获取,里面包含了这个的\r\n,但如果你的tcp客户端是通过\r\n进行分批次获取数据,需要额外的进行判断,组装数据
2:如果当你get一个不存在的键时,redis将会给你返回"$-1\r\n" 来表示该数据不存在,注意,它不是表达空字符串,而是表达它为null
多条批量回复
多条批量回复由"*" 开头,后面的数字代表共有几条回复,例如:
smembers listA
listA是本人已经add过的集合键名,数据如下:
将回复:
*5 $1 a $1 1 $12 1 2 a f $0 $1
由于\r\n太多,这里不处理为字符串显示
可看出:
*符号后面,是5,代表是5条回复 \\r\\n 后面跟着$1,代表是批量回复,1是跟着的字节 \\r\\n 读取1字节的a \\r\\n 继续读取$1,代表是批量回复,1是跟着的字节 ... 读取$0,代表是批量回复,0代表该键值为空 \\r\\n
注意事项
1:多条批量回复,也可能后面跟着一个*多条批量回复,比如在geohash里面: *1后面跟*2,或者可能*2后面跟着*2,代表这个回复,有2条回复,并且回复里面也有2条回复
2:$0代表着空字符串
3:$-1代表着null
php实现
本人使用swoole client协程客户端,已经实现了redis的通信协议,组件地址:https://github.com/easy-swoole/redis
核心处理代码如下:
代码使用swoole tcp客户端,配置为每次根据\r\n读取,每次读取到\r\n时返回
请求:
public function sendCommand(array $commandList) { $argNum = count($commandList); $str = "*{$argNum}\\r\\n"; foreach ($commandList as $value) { $len = strlen($value); $str = $str . '$' . "{$len}\\r\\n{$value}\\r\\n"; } return $this->send($str);//tcp流发送,本文不做说明,可查看源码 } $client->sendCommand(\['sadd', "listA", 'a'\]);
响应:
function recv(): ?Response { /* * 用单行回复,回复的第一个字节将是“+” 错误消息,回复的第一个字节将是“-” 整型数字,回复的第一个字节将是“:” 批量回复,回复的第一个字节将是“$” 多个批量回复,回复的第一个字节将是“*” */ $result = new Response(); $str = $this->client->recv($this->timeout); if (empty($str)) { $result->setStatus($result::STATUS_TIMEOUT); return $result; } /** * 去除每行的\\r\\n */ $str = substr($str, 0, -2); $op = substr($str, 0, 1); $result = $this->opHandel($op, $str); return $result; } /** * 字符串处理方法 * opHandel * @param $op * @param $value * @return Response * @author Tioncico * Time: 11:52 */ protected function opHandel($op, $value) { $result = new Response(); switch ($op) { case '+': { $result = $this->successHandel($value); break; } case '-': { $result = $this->errorHandel($value); break; } case ':': { $result = $this->intHandel($value); break; } case '$': { $result = $this->batchHandel($value); break; } case "*": { $result = $this->multipleBatchHandel($value); break; } } return $result; } /** * 状态类型处理 * successHandel * @param $value * @return Response * @author Tioncico * Time: 11:52 */ protected function successHandel($value): Response { $result = new Response(); $result->setStatus($result::STATUS_OK); $result->setData(substr($value, 1)); return $result; } /** * 错误类型处理 * errorHandel * @param $value * @return Response * @author Tioncico * Time: 11:53 */ protected function errorHandel($value): Response { $result = new Response(); //查看空格位置 $spaceIndex = strpos($value, ' '); //查看换行位置 $lineIndex = strpos($value, PHP_EOL); if ($lineIndex === false || $lineIndex > $spaceIndex) { $result->setErrorType(substr($value, 1, $spaceIndex - 1)); } else { $result->setErrorType(substr($value, 1, $lineIndex - 1)); } $result->setStatus($result::STATUS_ERR); $result->setMsg(substr($value, 1)); return $result; } /** * int类型处理 * intHandel * @param $value * @return Response * @author Tioncico * Time: 11:53 */ protected function intHandel($value): Response { $result = new Response(); $result->setStatus($result::STATUS_OK); $result->setData((int)substr($value, 1)); return $result; } /** * 批量回复处理 * batchHandel * @param $str * @param $timeout * @return bool|string * @author Tioncico * Time: 17:13 */ protected function batchHandel($str) { $response = new Response(); $strLen = substr($str, 1); //批量回复,继续读取字节 $len = 0; $buff = ''; if ($strLen == 0) { $this->client->recv($this->timeout); $response->setData(''); } else { while ($len < $strLen) { $strTmp = $this->client->recv($this->timeout); $len += strlen($strTmp); $buff .= $strTmp; } $response->setData(substr($buff, 0, -2)); } $response->setStatus($response::STATUS_OK); return $response; } /** * 多条批量回复 * multipleBatchHandel * @param $value * @return Response * @author Tioncico * Time: 14:33 */ protected function multipleBatchHandel($value) { $result = new Response(); $len = substr($value, 1); if ($len == 0) { $result->setStatus($result::STATUS_OK); $result->setData(\[\]); } elseif ($len == -1) { $result->setStatus($result::STATUS_OK); $result->setData(null); } else { $arr = \[\]; while ($len--) { $str = $this->client->recv($this->timeout); $str = substr($str, 0, -2); $op = substr($str, 0, 1); $response = $this->opHandel($op, $str); $arr\[\] = $response->getData(); } $result->setStatus($result::STATUS_OK); $result->setData($arr); } return $result; } $recv = $client->recv();
注,本文协议内容参考了https://blog.csdn.net/tanswer_/article/details/80846757文章