0. 写在前面
大家如果有去看过nodejs所支持的官方库的话,应该会惊讶于它所提供了非常完善的网络库,不仅是应用层,传输层,等等基础的协议,我们可以按照事件驱动的逻辑编写清晰易懂的网络应用,网络服务。这也是本文为什么选择Nodejs编写的原因。
1. 背景映入
大家在使用一些数据库软件的时候常常会使用远程连接
1 | mysql -h xxx.xxx.xxx.xx -u xzzz -p |
这里也指明了ip地址,但是很明显这里可不是http协议在服务,而是更加底层的协议 – 传输层协议,具体来说是TCP协议(Transmission Control Protocol)。通信的示意图如下:
所以很自然的想到,数据库的客户端一定经过如下流程,从而与远程相连接:
graph TB身份验证 –> 运输层连接建立运输层连接建立 –> 客户端服务端输入输出绑定_通道客户端服务端输入输出绑定_通道 –> 连接中断连接中断 –> 双方退出释放资源
所以我们可以尝试向服务端发送这样的请求消息,建立与服务端的连接,发送一些数据,接受一些数据,最后断开连接。
2. 数据库选择
这里为了简单起见,我们考虑不需要身份验证的redis数据库来作为此次实验的服务端。
如果大家是mac,或者linux倒是可以直接安装,如果是windows的话,推荐使用docker进行安装,这里给出一行docker命令。
1 | docker run --name redis-server -p 6379:6379 -d redis:latest |
3. Nodejs TCP连接
在nodejs中支持TCP连接的是net模块, 其中使用createConnection(config)
或者直接new Socket(config)
来初始化一个TCP连接。
上面两个函数不论哪一个都会返回socket实例,如果连接正常的话,就可以通过这个socket发送消息了。
当服务端redis接收到消息之后也会返回相应的消息,在本机客户端通过对数据的校验,检查后,触发相应的操作(是拒绝还是接受服务端的响应)。
4. 代码编写
知道了原理之后,我这里直接把代码贴出来
- RedisSocket: 继承自Socket
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 | class RedisSocket extends Socket { constructor(config: RedisClientConfig) { super (); this .connect(config.port, config.host); } // Set public set(key: string, value: string | number): Promise<buffer> { return new Promise((resolve, reject) => { this .write(`SET ${key} ${value}n`); const fetchAns = (chunk: Buffer) => { if (chunk.toString().includes( "OK" )) { resolve(chunk); this .off( "data" , fetchAns); // 在交付完成之后使用off 把函数取消绑定 } else { reject( "error! can't set data" ); } } this .on( "data" , fetchAns); }) } // Get public get(key: string): Promise<buffer> { return new Promise((resolve, reject) => { try { this .write(`GET ${key}n`); const fetchAns = (chunk: Buffer) => { resolve(chunk); this .off( "data" , fetchAns); // 在交付完成之后使用off 把函数取消绑定 } this .on( "data" , fetchAns); } catch (err) { reject(err); } }) } // 断开TCP public close() { this .end(); } } </buffer></buffer> |
这个类将用来处理建立好后的连接的
- RedisClient
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 | class RedisClient { private config: RedisClientConfig; constructor(config: RedisClientConfig) { this .config = config; // 配置项 } // 获取redis实例 getConnection(): Promise<redissocket> { return new Promise((resolve, reject) => { const socket = new RedisSocket( this .config); socket.on( "connect" , () => { resolve(socket); }); socket.on( "error" , (err) => { reject(err); }); }); } }</redissocket> |
这个类用来建立与服务端的连接,使用getConnection()
方法,将会交付一个redisSocket,使用这个Socket可以直接向server发送和接受数据。
5. 实验
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 | import { RedisClient, RedisSocket } from "./src/Client" ; const Redis = new RedisClient({ host: "localhost" , port: 6379 }); Redis.getConnection().then((socket: RedisSocket) => { socket.set( "Mushroom" , "Cookie" ); socket.set( "Mici" , "Icmi" ).then( () => { socket.get( "Mushroom" ).then((data: Buffer) => { console.log(data.toString()); socket.close(); }) }); }) |
这里使用RedisClient建立与本地redis的连接,随后通过getConnection()获取到连接实例,并通过这个连接实例设置了两个数据,以及获取了一数据并打印了出来。
1 2 3 | > pnpm dev > $6 // 这里的$6你也许会感到奇怪,不过我们很快就会知道这是什么 > Cookie |
6. wireshark 抓包分析
这一次请求就是一整个完整的TCP流程,
在这其中TCP保证数据的可靠传输,而RESP(REdis Serialization Protocol)把数据封装成一个fragment段,发送到下面的TCP
服务端相应的时候也是如此,会把数据封装起来发送到TCP中转发出去。
看看发送方的RESP
看看响应的RESP
所以知道了吗?没错,6其实就是长度那一部分强行转化为字符串的结果,所以在现在很多流行的redis客户端中如ioredis都对RESP报文做了非常完备的解析,这使得开发者能够非常丝滑的与redis服务端交互。(感谢这些开发者做的一切!)
7. 杂与代码
到此这篇关于用Nodejs 实现一个简单的 Redis客户端的文章就介绍到这了,更多相关Nodejs实现Redis客户端内容请搜索IT俱乐部以前的文章或继续浏览下面的相关文章希望大家以后多多支持IT俱乐部!