应用软件和数据库或者说和其它外部程序是怎么连接的呢?大部分同学应该都知道,基本都是通过 TCP 建立连接来进行通信的,不过也有像 ES 这种直接通过 HTTP 发请求来操作的。Redis 和大部分的数据库软件都是类似的,通过的是 TCP 连接的。另外还有 UnixSocket 这种形式,不过这种只能应用在本地。
此外,不同的软件在进行通信的时候,也经常会使用自己的一套通信规则,或者说叫做通信协议。比如说 Redis 使用的就是 RESP 这个协议。
其实呀,不管是 Redis 还是 MySQL ,都是类似的,只不过 MySQL 是使用的它自己的通信协议,而 Redis 的 RESP 相对来说会简单很多,我们自己随时就可以手写一个。当然,只是相对的简单,如果你真的想自己写一套非常完备的 Redis 客户端的话,那还是需要考虑更多因素的。
还记得我们在 Redis进阶:持久化策略https://mp.weixin.qq.com/s/s0jMUsDCqHcq9Plc1DoRvA 中看到过的那个 aof 文件中的内容吗?如果不记得了也没关系,咱们粘过来。
………………
*3^M
$3^M
set^M
$2^M
bb^M
$6^M
123123^M
………………
这一段是啥意思呢?其实这一段就是 set bb 123123
这一行命令的 RESP 协议表示。
* 表示命令参数的数量,这里 set、bb、123123 分别代表一个参数,结果就是 3
乱码那个内容其实是 \r\n ,也就是换行
$ 表示参数的长度,set 就是 $3
然后就是具体的参数内容
好了,一个 SET 命令就是这么简单,这是向 Redis 实例发送命令的过程。而响应返回的内容会更复杂一些,这里先简单介绍,后面看代码的时候再详细说明。
如果返回的是简单的单行字符串信息,会有一个 + 号,如 +OK
如果有错误信息,会有一个 - 号,如 -(error) ERR syntax error
如果是数字,会有一个 : 号,如 : 3
如果返回的是批量回复,第一行会有一个 $ 符号,如 $3
,第二行是具体的值
如果是多个批量回复,第一行会有一个 * 号,格式和上面发送命令的非常类似
看不懂啥意思?别急,我们先用 PHP 连接上,然后看效果就很明显了。
要使用 PHP 直接连接 Redis 服务,就需要使用 TCP 连接,因此,我们需要先建立连接。建立 socket 的连接方式很多,咱们就直接使用 stream_socket_client() 函数即可。
$redis = stream_socket_client('tcp://127.0.0.1:6379', $errno, $err, 5);
if (!$redis) {
die('连接失败: ' . $err);
}
这个像什么?是不是就像我们使用扩展的时候最开始的连接操作。
$redis = new Redis();
$redis->connect('127.0.0.1', 6379);
没错,它们本来就是一个概念,没啥区别,都是为了与 Redis 服务实例建立连接。接着呢?发送一个命令,然后读取响应嘛,标准的输入输出流程。实现的方式也很多,fread() 之类的都可以,不过简单起见,咱们还是使用 stream 系列函数。
$cmd = "*1\r\n$4\r\nPING\r\n";
stream_socket_sendto($redis, $cmd);
echo stream_socket_recvfrom($redis, 4096);
好了,写完了,测测。咱们向 Redis 实例发送了一个 PING 命令,格式就是按照上面的标准格式拼接的,首先需要一个 *1 表示命令有多少参数,然后加上换行 \r\n ,接着继续来个 $4 ,表示参数长度,这里是 PING ,正好是 4 个字符。注意,每一段都要加换行符。
➜ source git:(master) ✗ php 23.php
+PONG
嗯,效果满意。真的就这样,说实话,手撸 PHP 客户端的最核心的操作我们就已经完成了。建立连接、有输入,服务端也返回了输出。
看到了返回结果嘛?是不是前面有个 + 号。看来官方文档果然没骗我们啊,不过为啥 redis-cli 没有显示出这些符号呢?因为 redis-cli 也是一个客户端,它就是官方的命令行客户端,所以它对这些符号进行处理了。稍后我们也简单处理一下。
接下来再改造一下,让我们可以通过命令行输入命令,这样更好测试各种返回结果。
$cmd = "";
if (count($argv) > 1) {
$cmd = "*" . (count($argv) - 1) . "\r\n";
for ($i = 1; $i < count($argv); $i++) {
$cmd .= "$" . strlen($argv[$i]) . "\r\n";
$cmd .= $argv[$i] . "\r\n";
}
}
echo str_replace("\r\n", "\\r\\n",$cmd), PHP_EOL;
stream_socket_sendto($redis, $cmd);
echo stream_socket_recvfrom($redis, 4096);
通过 PHP 原生的命令行 $argv
参数,获得命令行中的所有参数信息,接着:
count($argv) - 1 是命令行中所有参数的数量,正好对应 RESP 协议中 * 符号需要的数量内容
遍历每个参数,组织 $ 符号表示的具体内容长度
将每个具体值也拼接上去
看看效果吧。
➜ source git:(master) ✗ php 23.php set a 123
*3\r\n$3\r\nset\r\n$1\r\na\r\n$3\r\n123\r\n
+OK
接着我们就来测测各种情况出现的可能性。
➜ source git:(master) ✗ php 23.php set a 123 123
*4\r\n$3\r\nset\r\n$1\r\na\r\n$3\r\n123\r\n$3\r\n123\r\n
-ERR syntax error
➜ source git:(master) ✗ php 23.php decr a
*2\r\n$4\r\ndecr\r\n$1\r\na\r\n
:122
➜ source git:(master) ✗ php 23.php get a
*2\r\n$3\r\nget\r\n$1\r\na\r\n
$3
122
➜ source git:(master) ✗ php 23.php lpush b 1 2 3 4
*6\r\n$5\r\nlpush\r\n$1\r\nb\r\n$1\r\n1\r\n$1\r\n2\r\n$1\r\n3\r\n$1\r\n4\r\n
:4
➜ source git:(master) ✗ php 23.php lrange b 0 -1
*4\r\n$6\r\nlrange\r\n$1\r\nb\r\n$1\r\n0\r\n$2\r\n-1\r\n
*4
$1
4
$1
3
$1
2
$1
1
看出来了吧,确实在报错的时候会有个 - 号,返回纯数字的时候会有个 : 号,如果是普通的 GET 会返回 $ 符号表示的数据长度和具体的内容,剩下的数组结果也不用多说了。
既然知道 RESP 返回的结果是怎样的了,那么我们就可以对结果再进行一个封装,去掉特殊符号,并且如果是数组的话,返回成一个 PHP 格式的数组。
$cmd = "";
if (count($argv) > 1) {
$cmd = "*" . (count($argv) - 1) . "\r\n";
for ($i = 1; $i < count($argv); $i++) {
$cmd .= "$" . strlen($argv[$i]) . "\r\n";
$cmd .= $argv[$i] . "\r\n";
}
}
if ($cmd) {
stream_socket_sendto($redis, $cmd);
$ret = stream_socket_recvfrom($redis, 4096);
$ret = explode("\r\n", trim($ret, "\r\n"));
if (count($ret) == 1) {
$ret = $ret[0];
if (strpos($ret, "+") !== false) $ret = str_replace("+", "成功:", $ret);
if (strpos($ret, "-") !== false) $ret = str_replace("-", "失败,原因是:", $ret);
if (strpos($ret, ":") !== false) $ret = str_replace(":", "操作数量:", $ret);
echo $ret, PHP_EOL;
} else {
if (strpos($ret[0], "*") !== false) {
$response = [];
$i = 0;
foreach($ret as $v){
if ($i == 0 || $i % 2 != 0 ) {
$i++;
continue;
}
$response[] = $v;
$i++;
}
print_r($response);
} else {
echo $ret[1], PHP_EOL;
}
}
}
一个非常简单的封装,首先按换行符把返回的结果分割成数组,如果只有一行数据,判断是成功、失败还是数量。如果不只有一行的话,判断第一行里面有 * 还是有 $ 符号,并进行相应的操作。
我们来试试看吧。
➜ source git:(master) ✗ php 23.php set a 123
成功:OK
➜ source git:(master) ✗ php 23.php decr a
操作数量:122
➜ source git:(master) ✗ php 23.php get a
122
➜ source git:(master) ✗ php 23.php lrange b 0 -1
Array
(
[0] => 4
[1] => 3
[2] => 2
[3] => 1
)
是不是有那么一点 Redis 客户端的味道了。我们这只是非常简单的封装,如果是 Hash、Sorted Set 之类的,这样直接返回一个数组肯定还是不行的。就像文章开始的时候我们说过的,如果真的要写一套完整的客户端,需要考虑的内容远比我们现在这样简单的写一个要多的多。不过总体来说,大概的功能和协议的规则相信各位也已经清楚了。
今天的内容比较简单吧,主要就是对 RESP 协议进行了一个全面的了解。至于 TCP 的连接部分,也是非常的简单和基础,没有什么更多要说的内容。通过 RESP 相关的学习,其实就是对整个 Redis 通信过程有了一个基础的了解,也对我们的扩展是如何去访问操作 Redis 的有了一个简单的了解。
参考文档:
https://redis.io/docs/reference/protocol-spec/