用nodejs写一个简单的Redis客户端

Nodejs cyanprobe 8年前 (2016-09-12) 3309次浏览 已收录 0个评论

前言:

今天一天在折腾用LUA脚本一次性返回车队集合(set)中的司机位置(hash),因为跨类型获取,新建多个redis实例循环可能更加耗费资源于是便想到用lua脚本来操作一次性返回所有内容,“all in the memory”,这样在redis的lua脚本解析器中执行能减去通信的消耗,lua不会写起来费劲,最后勉强采用神一样的拼接返回了。这个脚本语言真的是很灵活强大,如果未来专门折腾redis的话我愿意深入了解它。
闲言少叙,老雷在发自己新博文的时候我偶然看到了用redis写一个简单redis客户端这篇文章,一看通讯方式貌似很简单,Geemo那个狗逼曾经给我形容的非常非常难,我很久前问他我可以尝试写一下么,难么。狗逼是这么跟我说的:“老雷出的题你说呢,呵呵…”。狗日的,结果今天——

【传说】群主智障是狗
http://morning.work/page/2016-05/how-to-write-a-nodejs-redis-client.html 抓到了。今晚撸这个
【传说】geemo我的 2016/8/3 21:06:10草21:06:23
【传说】geemo我的 2016/8/3 21:06:23群主  我早撸过了
【传说】群主智障是狗 2016/8/3 21:06:41狗逼之前不告诉我怎么通信
【传说】geemo我的 2016/8/3 21:06:43而且我写了个交互式的
【传说】geemo我的 2016/8/3 21:07:21狗逼  你问了我吗

妈的渣渣,自己撸了,然后不告诉我,让我自己体会。(PS:最上面是老雷原文链接)这种人,怎一狗逼了得。

正文:

为了避免无耻照抄的行为,我还是按照老司机的步伐走一遍,总结下。
假如我要执行命令KEYS *,只要往服务器发送KEYS *\r\n即可,这时服务器会直接响应结果,返回的结果格式如下:
用单行回复,回复的第一个字节将是+
错误消息,回复的第一个字节将是-
整型数字,回复的第一个字节将是:
批量回复,回复的第一个字节将是$
多个批量回复,回复的第一个字节将是*
在win下用telnet命令链接服务,telnet命令需要在控制面板-》程序,打开或关闭win应用—》 telnet通讯
正常执行命令会出现如下反馈

//1、返回错误
help
-ERR unknown command ‘help’
//2、操作成功
set abc 123456
+OK
//3、得到结果
get abc
$6
123456
//4、得不到结果
get aaa
$-1
//5、得到的结果是整形数字
hlen aaa
:5
//6、数组结果
keys a*
*3
$3
abc
$3
aa1
$1
a
//7、多命令执行  可以看出是依次执行返回的
multi
+OK
get a
+QUEUED
get b
+QUEUED
get c
+QUEUED
exec
*3
$5
hello
$-1
$5
world

//结果解析类(手敲非复制)
class RedisProto{
    constructor () {
        this._lines = []; //第一次已经解析出来的行
        this._text=''; //不能构成一行的文本
    }
//添加到缓冲区
push(text) {
//结果安装\r\n进行分割
const lines=(this._text+text).split('\r\n');
//若结尾为'\r\n',那么数组最后一个元素一定为一个空字符串下面将其移除
this._text=lines.pop();
//将下一部分跟date事件相连
this._lines=this._lines.concat(lines);
  }
//解析下一个结果,若不存在返回null
next() {
    const lines =this._lines;
    const first=lines[0];
    //去掉指定数量的行返回结果
    const popResult = (lineNumber,result)=>{
        this._lines = this._lines.slice(lineNumber);
        return this.result = result;
    }
    //返回空结果
    const popEmpty = () => {
        return this.result=false;
    }
    if(lines.length<1)
        return popEmpty();
    switch(first[0]){
        case '+':
            return popResult(1,{data:first.slice(1)});
        case '-':
            return popResult(1,{data:first.slice(1)});
        case ':':
            return popResult(1,{data:Number(first.slice(1))});
        case '$':
            const n = Number(first.slice(1));
            if(n=== -1){
                //若为-1,无结果
                 return popResult(1, {data: null});
            }else{
                //选取下一行作为结果  判断是否为最后一行
                if(typeof second !== 'undefined'){
                   return popResult(2, {data: second});
                }else{
                    return popEmpty();
                }
            }
       case '*':{
           const n = Number(first.slice(1));
           if(n===0){
               return popResult(1, {data: []});
           } else {
           const array = [];
           let i = 1;
           for(; i < lines.length && array.length < n; i++) {
                const a = lines[i];
                const b = lines[i+1];
                if (a.slice(0,3) === '$-1') {
                    array.push(null);
                } else if (a[0] === ':') {
                    array.push(Number(a.slice(1)));
                } else {
                   if (typeof b !== 'undefined') {
                     array.push(b);
                     i++;
                    } else {
                        return popEmpty();
                    }
                }
           }
           if (array.length === n) {
                return popResult(i, {data: array});
            } else {
                return popEmpty();
            }
           }
       }
        default:
        return popEmpty();
    }
}
}
//导出
module.exports = RedisProto;
const proto = new RedisProto();
// 接受到数据
proto.push('*3\r\n$3\r\nabc\r\n$3\r\naa1\r\n$1\r\na\r\n');
proto.push('+OK\r\n');
while (proto.next()) {
  // proto.next() 如果有解析出完整的结果则返回结果,没有则返回 false
  // 可通过 proto.result 获得
  console.log(proto.result);
}

用NET模块实现nodejs与服务器之间的通讯。

//实现数据块链接 实体类
const events = require('events');
const net = require('net');
const RedisProto = require('./test2');
class Redis extends events.EventEmitter{
 constructor(options){
 super();
 // 默认连接配置
 options = options || {};
 options.host = options.host || '127.0.0.1';
 options.port = options.port || 6379;
 this.options = options;
 // 连接状态
 this._isClosed = false;
 this._isConnected = false;
 // 回调函数列表
 this._callbacks = [];
 this._proto = new RedisProto();
 this.connection = net.createConnection(options.port, options.host, () => {
 this._isConnected = true;
 this.emit('connect');
 });
 //注册事件
 this.connection.on('error', (err) => {
 this.emit('error', err);
 });
 this.connection.on('close', () => {
 this._isClosed = true;
 this.emit('close');
 });
 this.connection.on('end', () => {
 this.emit('end');
 });
//触发data事件
 this.connection.on('data', (data) => {
 this._pushData(data);
 });
 }
 // 发送命令给服务器,具体看老雷的文章,回调函数同时支持callback和promise
 sendCommand(cmd, callback) {
 return new Promise((resolve, reject) => {
 const cb = (err, ret) => {
 callback && callback(err, ret);
 err ? reject(err) : resolve(ret);
 };
 // 如果当前连接已断开,直接返回错误
 if (this._isClosed) {
 return cb(new Error('connection has been closed'));
 }
 // 将回调函数添加到队列
 this._callbacks.push(cb);
 // 发送命令
 this.connection.write(`${cmd}\r\n`);
 });
 }
 // 接收到数据,循环结果
 _pushData(data) {
 console.log(data.toString())
 this._proto.push(data);
 while (this._proto.next()) {
 const result = this._proto.result;
 const cb = this._callbacks.shift();
 if (result.error) {
 cb(new Error(result.error));
 } else {
 cb(null, result.data);
 }
 }
 }
 // 关闭连接
 end() {
 this.connection.destroy();
 }
}
const client = new Redis();
client.sendCommand('GET a', (err, ret) => {
 console.log('b=%s, err=%s', ret, err);
});
client.sendCommand('GET b', (err, ret) => {
 console.log('b=%s, err=%s', ret, err);
});
//打开本地redis试了试能用.....

后记:

这样简单的实现了redis客户端的功能,其实老雷下面提到了一些改进和方法(PS:还没沉下心思看),先发这些,全部手敲认证,看了看第一次编辑已经是一个月前了,天,我都干了什么!!回调队列那里还有点迷糊,明天问问geemo。。。


CyanProbe , 版权所有丨如未注明 , 均为原创丨本网站采用BY-NC-SA协议进行授权
转载请注明原文链接:用nodejs写一个简单的Redis客户端
喜欢 (1)
发表我的评论
取消评论

表情 贴图 加粗 删除线 居中 斜体 签到

Hi,您需要填写昵称和邮箱!

  • 昵称 (必填)
  • 邮箱 (必填)
  • 网址