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

文章目录
    分类:存储运维 标签:Redis

    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/

    视频链接

    微信文章地址:https://mp.weixin.qq.com/s/YJIZQLK264GZqlPQvRkbKQ

    B站视频地址:https://www.bilibili.com/video/BV1XN411a71U/

    微信视频地址:https://mp.weixin.qq.com/s/7p2nm0e15aGydPLtDbydnA

    搜索
    关注