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
Yii2.0源码阅读-PHP如何与redis通信?的更多相关文章
- Yii2.0源码阅读-一次请求的完整过程
Yii2.0框架源码阅读,从请求发起,到结束的运行步骤 其实最初阅读是从yii\web\UrlManager这个类开始看起,不断的寻找这个类中方法的调用者,最终回到了yii\web\Applicati ...
- Yii2.0源码阅读-从路由到控制器
之前的文章弄清了一次请求的开始到结束.主要讲了Yii Applicaton实例的创建.初始化,UrlManager如何返回Yii中的路由信息,到runAction,最后将Response发送给客户端. ...
- Yii2.0源码阅读-视图(View)渲染过程
之前的文章我们根据源码的分析,弄清了Yii如何处理一次请求,以及根据解析的路由如何调用控制器中的action,那接下来好奇的可能就是,我在控制器action中执行了return $this->r ...
- Yii2.0源码阅读-behavior的实现原理
Yii2.0中的一个思想就是组件化的思想,所以.大多数的类都直接或间接的继承自yii\base\Component,而组件的三大功能:属性.事件.行为. 行为的目的是为了方便的扩展一个类的功能,而不需 ...
- Yii2.0源码分析之——控制器文件分析(Controller.php)创建动作、执行动作
在Yii中,当请求一个Url的时候,首先在application中获取request信息,然后由request通过urlManager解析出route,再在Module中根据route来创建contr ...
- Vue2.0源码阅读笔记(四):nextTick
在阅读 nextTick 的源码之前,要先弄明白 JS 执行环境运行机制,介绍 JS 执行环境的事件循环机制的文章很多,大部分都阐述的比较笼统,甚至有些文章说的是错误的,以下为个人理解,如有错误, ...
- Vue2.0源码阅读笔记--生命周期
一.Vue2.0的生命周期 Vue2.0的整个生命周期有八个:分别是 1.beforeCreate,2.created,3.beforeMount,4.mounted,5.beforeUpdate,6 ...
- Vue2.0源码阅读笔记--双向绑定实现原理
上一篇 文章 了解了Vue.js的生命周期.这篇分析Observe Data过程,了解Vue.js的双向数据绑定实现原理. 一.实现双向绑定的做法 前端MVVM最令人激动的就是双向绑定机制了,实现双向 ...
- Vue2.0源码阅读笔记(二):响应式原理
Vue是数据驱动的框架,在修改数据时,视图会进行更新.数据响应式系统使得状态管理变的简单直接,在开发过程中减少与DOM元素的接触.而深入学习其中的原理十分有必要,能够回避一些常见的问题,使开发变的 ...
随机推荐
- Hibernate单表操作
单一主键 assigned:由Java应用程序负责生成(即手工的赋值) native:由底层的数据库自动的生成标示符,如果是MySQL就是auto_increment,如果是Oracle就是seque ...
- Servlet之Listener监听器
Servlet2.5规范共有8中Listener接口,6种Event类型 ServletContextListener接口 [接口方法] contextInitialized()与 contextDe ...
- (NO.00005)iOS实现炸弹人游戏(七):游戏数据的序列化表示
大熊猫猪·侯佩原创或翻译作品.欢迎转载,转载请注明出处. 如果觉得写的不好请告诉我,如果觉得不错请多多支持点赞.谢谢! hopy ;) 用plist列表文件来表示游戏数据 因为在这个炸弹人游戏中有很多 ...
- 物料事务处理interface与temp解析
MTL_TRANSACTIONS_INTERFACE MTL_TRANSACTIONS_INTERFACE is the interface point between non– Inventory ...
- redhat安装vsftpd
一个小问题 rpm -qa|ftp 但是出现3个ftp 只安装了一个 关于网卡ip 首先,我们看到 网卡上面有个x 说明网络是有问题的 我们双击,看到 我们先把connected和connect at ...
- 解决 RtlCreateActivationContext() failed 0xc000000d
gtest 示例的Debug版启动报错: Debug输出如下: 'sample1_unittest.exe': Loaded 'D:\LibSrc\gtest_1.7.0_build\Debug\sa ...
- Uva - 12174 - Shuffle
用滑动窗口的思想,用一个数组保存每个数在窗口中出现的次数.再用一个变量记录在窗口中恰好出现一次的的数的个数,这样可以枚举所有可能的答案,判断它所对应的所有串口,当且仅当所有的串口均满足要求时这个答案可 ...
- ant+eclipse知识点详解及使用案例
ant的优点和地位就不再阐述,下面直接上知识点: 在java中使用xml文件开发,有以下基本语法 (1)project:每个ant程序有且只有一个此标签,而且是类似于html的总标签,有name,de ...
- 浅谈OC内存管理
一.基本原理 (一)为什么要进行内存管理. 由于移动设备的内存极其有限,所以每个APP所占的内存也是有限制的,当app所占用的内存较多时,系统就会发出内存警告,这时需要回收一些不需要再继续使用的内存空 ...
- awk字符串函数及其意义
awk字符串函数及其意义 awk提供了强大的内置字符串函数,用于实现文本的字符串替换.查找以及分隔等功能. awk字符串函数主要有:gsub.index.length.match.split.sub ...