【Redis26】Redis进阶:RESP协议-使用PHP手撸一个客户端

Redis进阶:RESP协议-使用PHP手撸一个客户端

应用软件和数据库或者说和其它外部程序是怎么连接的呢?大部分同学应该都知道,基本都是通过 TCP 建立连接来进行通信的,不过也有像 ES 这种直接通过 HTTP 发请求来操作的。Redis 和大部分的数据库软件都是类似的,通过的是 TCP 连接的。另外还有 UnixSocket 这种形式,不过这种只能应用在本地。

此外,不同的软件在进行通信的时候,也经常会使用自己的一套通信规则,或者说叫做通信协议。比如说 Redis 使用的就是 RESP 这个协议。

其实呀,不管是 Redis 还是 MySQL ,都是类似的,只不过 MySQL 是使用的它自己的通信协议,而 Redis 的 RESP 相对来说会简单很多,我们自己随时就可以手写一个。当然,只是相对的简单,如果你真的想自己写一套非常完备的 Redis 客户端的话,那还是需要考虑更多因素的。

RESP 协议

还记得我们在 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 连接与操作

要使用 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/

你可能感兴趣的:(redis,php,数据库,缓存,开发语言)