Yii2.0源码阅读-PHP如何与redis通信?

PHP与Redis可以通过socket进行通信,前提是PHP需要实现Redis的协议

RESP协议描述:

    • 字符串 \r\n : 表示一个正确的状态信息,具体信息是'+’后面的字符(Simple Strings)
    • 错误前缀 错误信息 \r\n : 表示一个错误信息,具体信息是当前行'-'后面的字符(Errors)
  • $ 字符串的长度 \r\n 字符串 \r\n : 表示字符串(Bulk Strings)
    • 数组元素个数 \r\n 其他所有类型 : 表示消息体总共有多少行(array)
  • : 数字\r\n:表示返回一个数值,:后面是相应的数字 (integer)

详细描述参考:https://redis.io/topics/protocol

1、PHP与redis建立连接

通过PHP的stream_socket_client函数可以建立一个socket连接,然后PHP就可以通过组装符合Redis协议格式的字符串,然后将消息发送给Redis

所以建立连接的代码如下:

    public function open()
    {
        if($this->_socket !== false){
            return;
        }
        //socket要连接的地址
        $remoteSocket = 'tcp://127.0.0.1:6379';
        //socket连接建立超时时间
        $timeout = ini_get('default_socket_timeout');
        //创建socket连接
        $this->_socket = @stream_socket_client($remoteSocket, $errorNumber, $errorDescription, $timeout, STREAM_CLIENT_CONNECT);
    }

2、执行redis操作命令

执行操作命令首先根据协议进行命令的构造,比如我们执行SET name xiaoming,那么对应在PHP中调用函数的方式可能是setValue($key, $value),PHP是怎么处理的呢?

首先,这一条set命令包含了多个字段:set、key、value,可能还有expire过期时间,所以需要用到协议中的也就是Redis的RESP Arrays,可以包含多个字符串,如果没有过期时间,那么这个set操作转换之后的命令就是:

$command = "*3\r\n$3\r\nSET\r\n$4\r\nname\r\n$8\r\nxiaoming\r\n”;

解释一下就是:

  • 消息数组包含三个元素
  • 第一个是个字符串,长度为3,值为SET
  • 第二个是个字符串,长度为4,值为name
  • 第三个是个字符串,长度为8,值为xiaoming

然后写入socket:

fwrite($this->_socket, $command);

这样一条set key value的命令就执行完成了

这里要注意的一点是,关于命令中字符串长度的计算,Redis文档中的描述:* A "$" byte followed by the number of bytes composing the string (a prefixed length), terminated by CRLF. 也就是说这个长度是按照byte字节来计算的,计算字符串有多少个字节,那么在php中计算字符串长度的时候就不能简单的用strlen了,而需要使用mb_strlen($str, '8bit'),来计算

3、解析Redis返回的消息

在向socket发送了消息之后,Redis执行之后会返回一些信息,同样写入这个socket中,我们要做的是按照协议格式进行消息的解析:

$line = fgets($this->_socket);

$line[0]就是消息的类型,对应上面协议中的:+、 - 、 $ 、 * 、 : 这五种

根据类型的不同再对$line剩余的部分进行解析

  • 如果为+,正确的消息,PONG | OK 返回true,否则返回redis返回的内容
  • 如果为-,错误的消息,说明Redis那边执行这条命令发生了错误,应该抛出异常
  • 如果为$,返回的是个字符串,先获取字符串的长度,也就是紧跟在$后面的数字,然后向后读取相应长度的字符串
  • 如果为:,返回的是数字,直接返回就好
  • 如果是,是个数组,解析获得数组中元素的个数,也就是紧跟在后面的数字,然后,递归的去解读每一行

4、完整的示例代码

class Redis
{
    //保存socket连接
    private $_socket = false;

    //redis server 的地址,可以是ip或者主机名
    public $hostname = '127.0.0.1';

    //端口
    public $port = 6379;

    //redis登录密码
    public $password;

    //redis 数据库 默认为0
    public $database = 0;
   
    /**
     * 建立一个Redis socket连接
     */
    public function open()
    {
        if($this->_socket !== false){
            return;
        }
        //socket要连接的地址
        $remoteSocket = 'tcp://' . $this->hostname . ':' . $this->port;
        //socket连接建立的超时时间
        $timeout = ini_get('default_socket_timeout');
        //创建socket连接
        $this->_socket = @stream_socket_client($remoteSocket, $errorNumber, $errorDescription, $timeout, STREAM_CLIENT_CONNECT);
        if($this->_socket){
            //如果有密码,使用密码以授权访问
            if($this->pasword){
                $this->executeCommand('AUTH', [$this->password]);
            }
            //选择数据库
            $this->executeCommand('SELECT', [$this->database]);
        }else{
            throw new Exception("创建redis连接失败");
        }
    }
    /**
     * 关闭与Redis的socket连接
     */
    public function close()
    {
        $this->executeCommand('QUIT');
        stream_socket_shutdown($this->_socket, STREAM_SHUT_RDWR);
        $this->_socket = null;
    }
    /**
     * 执行Redis命令
     */
    public function executeCommand($name, $params = [])
    {
        $this->open();
        //操作命令加到params中,以计算数组元素个数,也就是消息总共多少行
        array_unshift($params, $name);
        //按照redis通信协议组装消息
        $command = '*' . count($params) . "\r\n";
        foreach($params as $arg){
            $command .= '$' . mb_strlen($arg, '8bit') . "\r\n" . $arg . "\r\n";
        }
        //向socket中写入消息
        fwrite($this->_socket, $command);

        return $this->parseResponse(implode(' ', $params));
    }
    /** 
     * 解析Redis返回的信息
     */
    public function parseResponse($command)
    {
        if(($line = fgets($this->_socket)) === false){
            throw new Exception("redis socket 没有返回任何数据");
        } 
        //根据redis协议解析返回的消息
        $type = $line[0];
        //去除末尾的\r\n
        $line = mb_substr($line, 1, -2, '8bit');
        //按照type解析redis返回的消息类型
        switch($type){
            //返回的是正确的消息
            case '+':
                if($line == 'OK' || $line == 'PONG'){
                    return true;
                }else{
                    return $line;
                }
            //返回的是错误的消息
            case '-':
                throw new Exception("Redis error: $line");
            //返回的是一个数字
            case ':':
                return $line;
            //返回的是一个字符串
            case '$':
                //根据redis协议,如果返回的是-1 代表null "Null Bulk String"
                if($line == -1){
                    return null;
                }
                //读取字符串

                /**
                 * 加2是因为,字符串的协议是:$字符串长度字符串\r\n
                 * 也就是+2 加的是\r\n的长度
                 */
                $length = $line + 2;
                //读取的数据
                $data = '';
                //这里用while循环处理字符串长度为0的情况,后面可能有多个\r\n,如文档中的:"$0\r\n\r\n"
                while($length > 0){
                    if(($block = fread($this->_socket, $length)) === false){
                        throw new Exception("读取redis返回的字符串消息失败");
                    }
                    $data .= $block;
                    //长度减去$length,如果为空那就是减去一个\r\n,也就是2
                    $length -= mb_strlen($block, '8bit');
                }
                return mb_substr($data, 0, -2, '8bit');
            //返回的是一个数组消息
            case '*':
                $count = (int)$line;
                $data = [];
                for($i = 0; $i < $count; $i++){
                    $data[] = $this->parseResponse($command);
                }
                return $data;
            default:
                throw new Exception("redis返回了错误的消息标志");
        }
    }
    //get 命令示例
    public function getValue($key)
    {
        return $this->executeCommand('GET', [$key]);
    }
    //set 命令示例
    public function setValue($key, $value, $expire)
    {
        if($expire == 0){
            return $this->executeCommand('SET', [$key, $value]);
        }else{
            $expire = (int)$expire*1000;
            return $this->executeCommand('SET', [$key, $value, 'PX', $expire]);
        }
    }

}
//使用示例

$redis = new Redis();
$redis->setValue("blank","\r\n\r\n",100);

更多的命令在Yii2.0中的实现方式是定义一个数组,里面包含了所有的Redis操作命令,然后实现了__call方法,在__call中判断命令是否存在于数组中,存在则直接executeCommand

参考:Yii2.0的Redis实现

原文链接http://www.cnblogs.com/skyfynn/p/8980322.html