前言:
今天一天在折腾用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狗逼 你问了我吗
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
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。。。