zoukankan      html  css  js  c++  java
  • Yii2.0源码阅读-PHP如何与redis通信?

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

    RESP协议描述:

      • 字符串 : 表示一个正确的状态信息,具体信息是'+’后面的字符(Simple Strings)
      • 错误前缀 错误信息 : 表示一个错误信息,具体信息是当前行'-'后面的字符(Errors)
    • $ 字符串的长度 字符串 : 表示字符串(Bulk Strings)
      • 数组元素个数 其他所有类型 : 表示消息体总共有多少行(array)
    • : 数字 :表示返回一个数值,:后面是相应的数字 (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
    $3
    SET
    $4
    name
    $8
    xiaoming
    ”;
    

    解释一下就是:

    • 消息数组包含三个元素
    • 第一个是个字符串,长度为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) . "
    ";
            foreach($params as $arg){
                $command .= '$' . mb_strlen($arg, '8bit') . "
    " . $arg . "
    ";
            }
            //向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];
            //去除末尾的
    
            $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是因为,字符串的协议是:$字符串长度字符串
    
                     * 也就是+2 加的是
    的长度
                     */
                    $length = $line + 2;
                    //读取的数据
                    $data = '';
                    //这里用while循环处理字符串长度为0的情况,后面可能有多个
    ,如文档中的:"$0
    
    "
                    while($length > 0){
                        if(($block = fread($this->_socket, $length)) === false){
                            throw new Exception("读取redis返回的字符串消息失败");
                        }
                        $data .= $block;
                        //长度减去$length,如果为空那就是减去一个
    ,也就是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","
    
    ",100);
    

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

    参考:Yii2.0的Redis实现

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

  • 相关阅读:
    LeetCode算法题-Binary Search(Java实现)
    LeetCode算法题-Kth Largest Element in a Stream(Java实现)
    LeetCode算法题-Search in a Binary Search Tree(Java实现)
    LeetCode算法题-Degree of an Array(Java实现)
    LeetCode算法题-Count Binary Substrings(Java实现)
    LeetCode算法题-Binary Number with Alternating Bits(Java实现)
    之前见汤姆大叔 写过一系列的 js 深入理解 呢 很是感觉经典
    xsd 和 wsdl
    发现 一个业务管理系统 解决了 orm 和 前端框架 剩下的 是 业务逻辑 了 。 哈
    uwp 嗯 我最近 准备开发一个 应用 。 嗯 走起
  • 原文地址:https://www.cnblogs.com/skyfynn/p/8980322.html
Copyright © 2011-2022 走看看