描述

redis是一个经典的key-value缓存数据库,采用C/S架构。当我们安装成功以后,你就知道它有个服务端,启动后默认监听6379端口,然后提供一个客户端工具redis-cli。

我们可以使用redis-cli然后书写命令与服务端通信。

上面我们大概知道了redis的工作模式,为了更好的认知它,我就开始思考如何自己去连接服务端呢?我想到使用Yii2时,用到redis我是没有安装官方提供的redis扩展,但是它仍然可以与redis服务端通信,于是乎便去追踪了Yii2-redis组件的源代码,看完以后,深感作者的强大。

准备

看了大佬的代码,自己也想去实现一下,于是乎也开始找一些资料。
  • 学习redis协议【http://redis.cn/topics/protocol】

    1. Redis在TCP端口6379上监听到来的连接,客户端连接到来时,Redis服务器为此创建一个TCP连接。在客户端与服务器端之间传输的每个Redis命令或者数据都以\r\n结尾
    2. 新的统一协议已在Redis 1.2中引入,但是在Redis 2.0中,这就成为了与Redis服务器通讯的标准方式。
  • 理解协议并参照大佬自己去写

    代码有很多注释,看完协议,跟着代码就能明白了

<?php

/**
* Class SocketException
*/
class SocketException extends \Exception
{
/**
* @var array
*/
public $errorInfo = []; /**
* @return string
*/
public function getName()
{
return 'Redis Socket Exception';
} /**
* SocketException constructor.
* @param string $message
* @param array $errorInfo
* @param int $code
* @param Throwable|null $previous
*/
public function __construct($message = "", $errorInfo = [], $code = 0, Throwable $previous = null)
{
$this->errorInfo = $errorInfo;
parent::__construct($message, $code, $previous);
} /**
* @return string
*/
public function __toString()
{
return parent::__toString() . PHP_EOL . 'Additional Information:' . PHP_EOL . print_r($this->errorInfo, true);; // TODO: Change the autogenerated stub
}
} /**
* Class RedisConnection
*/
class RedisConnection
{
/**
* @var string
*/
public $host = 'localhost'; /**
* @var int
*/
public $port = 6379; /**
* @var null
*/
public $password = null;
/**
* @var int
*/
public $database = 0;
/**
* @var
*/
public $connectionTimeout;
/**
* @var null
*/
public $dataTimeout = null;
/**
*
* STREAM_CLIENT_ASYNC_CONNECT指示应打开每个后续连接,而不必等待上一个连接的完成
* @var int
*/
public $socketClientFlags = STREAM_CLIENT_CONNECT; /**
* @var array
*/
protected $pools = []; /**
* @var int
*/
protected $maxPoolSize = 10; /**
* RedisConnection constructor.
* @param string $host
* @param int $port
* @param null $password
*/
public function __construct($host = 'localhost', $port = 6379, $password = null)
{
$this->host = $host;
$this->port = $port;
$this->password = $password;
} /**
* @return string
*/
public function getConnectionString()
{
return 'tcp://' . $this->host . ':' . $this->port;
} /**
* @param int $database
*/
public function connect($database = 0)
{
$this->database = $database;
$countSize = count($this->pools);
if ($countSize > $this->maxPoolSize) {
return;
} if ($this->getSocket() !== false) {
return;
} $connId = $this->getConnectionString();
$connection = $connId . ', database=' . $this->database;
try {
$socket = stream_socket_client(
$connId,
$errorNumber,
$errorDescription,
$this->connectionTimeout ?? ini_get('default_socket_timeout'),
$this->socketClientFlags
); if ($socket) {
$this->pools[$connId] = $socket;
if ($this->dataTimeout !== null) {
$timeout = (int)$this->dataTimeout;
$microTimeout = (int)(($this->dataTimeout - $timeout) * 1000000);
stream_set_timeout($socket, $timeout, $microTimeout);
} if ($this->password !== null) {
$this->exec('AUTH', [$this->password]);
} if ($this->database !== null) {
$this->exec('SELECT', [$this->database]);
}
} else {
$message = "无法打开redis数据库连接 ($connection): $errorNumber - $errorDescription";
throw new Exception($message, $errorDescription, $errorNumber);
}
} catch (Exception $e) {
exit($e->getMessage());
}
} /**
* 用单行回复,回复的第一个字节将是“+”
* 错误消息,回复的第一个字节将是“-”
* 整型数字,回复的第一个字节将是“:”
* 批量回复,回复的第一个字节将是“$”
* 多个批量回复,回复的第一个字节将是“*”
* 命令LRNGE需要返回多个值
* LRANGE mylist 0 3
* 无值:*0
* 有值:
* *3
* $1
* c
* $1
* b
* $1
* a
* sadd mylist a b c d
* :4
*
* @link http://redis.cn/topics/protocol
* @param $cmd
* @param array $params
*/
public function exec($cmd, $params = [])
{
//状态:-ERR Client sent AUTH, but no password is set
//状态:+OK
//get: $15\r\nlemon1024026382\r\n
//del: :1
$params = array_merge(explode(' ', $cmd), $params);
$command = '';
$paramsCount = 0;
foreach ($params as $param) {
if ($param === null) {
continue;
}
$command .= '$' . mb_strlen($param, '8bit') . "\r\n" . $param . "\r\n";
$paramsCount++;
}
$command = '*' . $paramsCount . "\r\n" . $command;
// echo 'Executing Redis Command:', $cmd, PHP_EOL;
// echo 'Yuan Shi Redis Cmd:', $command, PHP_EOL;
return $this->send($command, $params);
} /**
* @param $cmd
* @param $params
* @return null
* @throws SocketException
*/
private function send($cmd, $params)
{
$socket = $this->getSocket();
$written = fwrite($socket, $cmd);
if ($written === false) {
throw new SocketException("无法写入到socket.\nRedis命令是: " . $cmd);
} return $this->parseData($params, $cmd);
} /**
* 用单行回复,回复的第一个字节将是“+”
* 错误消息,回复的第一个字节将是“-”
* 整型数字,回复的第一个字节将是“:”
* 批量回复,回复的第一个字节将是“$”
* 多个批量回复,回复的第一个字节将是“*”
* @link http://redis.cn/topics/protocol
*
* \r\n作为分割符号,使用fgets按行读取
* 如果是状态返回的命令或者错误返回或者返回整数这些相对简单处理
* 批量返回时,如果是*号,第一步先读取返回的计数器,比如keys * 会返回有多少个key值,循环计数器
* ,读取第二行就是$数字(批量回复),例如$9表示当前key的value值长度
* 代码里有 $length = intval($line)+2; +2其实是表示\r\n这个分隔符
* @param array $params
* @throws SocketException
* @return null|string|mixed
*/
private function parseData($params)
{
$socket = $this->getSocket();
$prettyCmd = implode(' ', $params);
if (($line = fgets($socket)) === false) {// 从文件指针中读取一行,这里最合适,因为协议分割标识是\r\n
throw new SocketException("无法从socket读取.\nRedis命令是: " . $prettyCmd);
} echo '服务端响应数据:', $line, PHP_EOL;
$type = $line[0];
$line = mb_substr($line, 1, -2, '8bit');
if ('+' === $type) {
// eg:SET ping select quit auth...
if (in_array($line, ['Ok', 'PONG'])) {
return true;
} else {
return $line;
}
} if ('-' === $type) {
throw new SocketException("Redis 错误: " . $line . "\nRedis命令是: " . $prettyCmd);
} // eg: hset zadd del
if (':' === $type) {
return $line;
} /**
* 例如:
* 输入存在的key:get jie 返回:$9\r\nyangjiecheng\r\n
* 输入:keys * 返回: *9\r\n$9\r\nchat.user\r\n$3\r\njie\r\n$4\r\ntest\r\n
* 输入不存在的key: get 111 返回:$-1 对应redis返回:nil
*
*/
if ('$' === $type) {
if (-1 == $line) {
return null;
} $length = intval($line) + 2; //+2 表示后面的\r\n
$data = '';
while ($length > 0) {
$str = fread($socket, $length);
if ($str === false) {
throw new SocketException("无法从socket读取.\nRedis命令是: " . $prettyCmd);
}
$data .= $str;
$length -= mb_strlen($str, '8bit');//此函数可以指定字符串编码并计算长度,8bit就是按照字节计算长度
} return !empty($data) ? mb_substr($data, 0, -2, '8bit') : '';
} if ('*' === $type) {
$count = intval($line);
$data = [];
for ($i = 0; $i < $count; $i++) {
$data[] = $this->parseData($params);
} return $data;
} throw new SocketException('从Redis读取到未能解析的数据: ' . $line . "\nRedis命令是: " . $prettyCmd);
} /**
* @return bool|mixed
*/
public function getSocket()
{
$id = $this->getConnectionString();
return isset($this->pools[$id]) ? $this->pools[$id] : false;
} /**
*
*/
public function close()
{
$connectionString = $this->getConnectionString();
foreach ($this->pools as $socket) {
$connection = $connectionString . ', database=' . $this->database;
echo 'Closing DB connection: ' . $connection . PHP_EOL;
try {
$this->exec('QUIT');
} catch (SocketException $e) {
// ignore errors when quitting a closed connection
}
fclose($socket);
} $this->pools = [];
} /**
*
*/
public function __destruct()
{
// TODO: Implement __destruct() method.
$this->close();
}
} /**
* @param $file
* @param $content
*/
function writeLog($file, $content)
{
$str = '@@@@@@@@@@ Time Is ' . date('Y-m-d H:i:s') . ' @@@@@@@@@' . PHP_EOL;
$str .= $content . PHP_EOL;
$str .= '@@@@@@@@@@ End Block Log @@@@@@@@' . PHP_EOL;
file_put_contents($file, $str, FILE_APPEND);
} set_exception_handler(/**
* @param Exception $exception
*/ function (\Exception $exception) {
echoMsg($exception->getMessage());
}); /**
* @param $msg
* @param string $type
*/
function echoMsg($msg, $type = 'error')
{
if ($type === 'error') {
$msg = "\e[" . "1;37m" . $msg . "\e[0m";
} else {
$msg = "\e[" . "0;31m" . $msg . "\e[0m";
}
echo "\033[" . "41m", "Exception: ", $msg, "\033[0m", PHP_EOL;
} set_error_handler(/**
* @param $errno
* @param $errstr
* @param $errfile
* @param $errline
*/ function ($errno, $errstr, $errfile, $errline){
echoMsg("custom error:[$errno] $errstr");
echoMsg(" Error on line $errline in $errfile");
});
//register_shutdown_function
$redis = new RedisConnection();
$redis->connect();
echo $redis->getConnectionString(), PHP_EOL;
//$redis->exec('SET', ['RedisClient', 'OK']);
//$redis->exec('DEL', ['RedisClient']);
//$redis->exec('KEYS', ['*']);
var_dump($redis->exec('GET', ['access_token']));
//var_dump($redis->exec('LRANGE', ['qlist', 0, 3]));

基于PHPstream扩展手动实现一个redis客户端的更多相关文章

  1. 学习T-io框架,从写一个Redis客户端开始

    前言   了解T-io框架有些日子了,并且还将它应用于实战,例如 tio-websocket-server,tio-http-server等.但是由于上述两个server已经封装好,直接应用就可以.所 ...

  2. RN如何基于js代码手动打一个main.jsbundle

    react-native bundle --entry-file index.js --bundle-output ./ios/bundle/main.jsbundle --platform ios ...

  3. spring整合redis客户端及缓存接口设计(转)

    一.写在前面 缓存作为系统性能优化的一大杀手锏,几乎在每个系统或多或少的用到缓存.有的使用本地内存作为缓存,有的使用本地硬盘作为缓存,有的使用缓存服务器.但是无论使用哪种缓存,接口中的方法都是差不多. ...

  4. Redis客户端命令

    Redis客户端命令 Redis 命令用于在 redis 服务上执行操作. 要在 redis 服务上执行命令需要一个 redis 客户端.Redis 客户端在我们之前下载的的 redis 的安装包中. ...

  5. spring整合redis客户端及缓存接口设计

    一.写在前面 缓存作为系统性能优化的一大杀手锏,几乎在每个系统或多或少的用到缓存.有的使用本地内存作为缓存,有的使用本地硬盘作为缓存,有的使用缓存服务器.但是无论使用哪种缓存,接口中的方法都是差不多. ...

  6. 用C、python手写redis客户端,兼容redis集群 (-MOVED和-ASK),快速搭建redis集群

    想没想过,自己写一个redis客户端,是不是很难呢? 其实,并不是特别难. 首先,要知道redis服务端用的通信协议,建议直接去官网看,博客啥的其实也是从官网摘抄的,或者从其他博客抄的(忽略). 协议 ...

  7. 一个基于chrome扩展的自动答题器

    1.写在前面 首先感谢小茗同学的文章-[干货]Chrome插件(扩展)开发全攻略, 基于这篇入门教程和demo,我才能写出这款 基于chrome扩展的自动答题器. git地址: https://git ...

  8. jmind-redis一个redis的nio客户端

    Redis是一个基于key/value的系统.Redis目前最新版本是2.2.4,用着很不错,不过java版本的客户端比较的不给力,目前redis 客户端jedis 是基于io 的socket . 而 ...

  9. 用Nodejs 实现一个简单的 Redis客户端

    目录 0. 写在前面 1. 背景映入 2. 数据库选择 3. Nodejs TCP连接 3. 代码编写 4. 实验 5. wireshark 抓包分析 6. 杂与代码 0. 写在前面 大家如果有去看过 ...

  10. 探索Redis设计与实现13:Redis集群机制及一个Redis架构演进实例

    本文转自互联网 本系列文章将整理到我在GitHub上的<Java面试指南>仓库,更多精彩内容请到我的仓库里查看 https://github.com/h2pl/Java-Tutorial ...

随机推荐

  1. ChannelInboundHandlerAdapter和SimpleChannelInboundHandler区别

    ChannelInboundHandlerAdapter和SimpleChannelInboundHandler是我们在使用Netty处理Handler时候很常用的两个继承类,虽然说二者实现的功能大致 ...

  2. Docker使用:利用宝塔面板Docker管理器快速搭建PHP、Java、Python、nodejs等配套运行环境

    思路:阿里云购买服务器选择centos7宝塔系统做宿主机,登录宝塔安装Docker管理器,获取一个centos7镜像,创建容器在里面再安装个宝塔后部署PHP.Python等. 点击购买阿里云云服务器, ...

  3. 计算机视觉中由基本矩阵F或者本质矩阵E计算摄像机投影矩阵时,经常提到“相差一个尺度因子”的含义

    在通过二维像素坐标恢复三维坐标的过程中,经常出现这个齐次坐标系.尺度不变性的概念.这篇博客讲的比较好. 一.关于齐次坐标系的直观感受 在我们的世界里,两平行线是永远不会相交的,但是在投影空间里,两条平 ...

  4. [转]Spring Security打造一个简单Login登录页面,实现登录+跳转+注销+角色权限功能,核心代码不到100行!

    原文链接:Spring Security打造一个简单Login登录页面,实现登录+跳转+注销+角色权限功能,核心代码不到100行!

  5. 史上最通俗Netty入门长文:基本介绍、环境搭建、动手实战

    原作者江成军,原题"还在被Java NIO虐?该试试Netty了",收录时有修订和改动. 1.阅读对象 本文适合对Netty一无所知的Java NIO网络编程新手阅读,为了做到这一 ...

  6. Python连接远程设备

    import paramiko def content_ssh(): ssh = paramiko.SSHClient() ssh.set_missing_host_key_policy(parami ...

  7. WxPython跨平台开发框架之使用PyInstaller 进行打包处理

    使用PyInstaller 打包Python项目是一个常见的需求,它可以将Python程序及其所有依赖项打包成一个独立的可执行文件或者安装文件,方便在没有安装Python环境的机器上运行.本随笔介绍W ...

  8. Java工具类SignUtil

    import javax.crypto.Mac; import javax.crypto.SecretKey; import javax.crypto.spec.SecretKeySpec; impo ...

  9. CDS标准视图:功能位置描述 I_FunctionalLocationText

    视图名称:功能位置描述 I_FunctionalLocationText 视图类型:基本视图 视图代码: 点击查看代码 @EndUserText.label: 'Functional Location ...

  10. linux:网络

    网络概念 网络发展 1.1969年互联网元年 2.局域网(LAN,Local Area Network).城域网(MAN).广域网(WAN,Wide Area Network) ip地址 网络基础命令 ...