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


目录
  • nodejs所支持的官方库的话,应该会惊讶于它所提供了非常完善的网络库,不仅是应用层,传输层,等等基础的协议,我们可以按照事件驱动的逻辑编写清晰易懂的网络应用,网络服务。这也是本文为什么选择Nodejs编写的原因。

    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命令。

    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接收到消息之后也会返回相应的消息,在本机客户端通过对数据的校验,检查后,触发相应的操作(是拒绝还是接受服务端的响应)。

    3. 代码编写

    知道了原理之后,我这里直接把代码贴出来

    • RedisSocket: 继承自Socket
    class RedisSocket extends Socket {
        constructor(config: RedisClientConfig) {
            super();
            this.connect(config.port, config.host);
        }
    	// Set
        public set(key: string, value: string | number): Promise {
            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 {
            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();
        }
    }
    

    这个类将用来处理建立好后的连接的

    • RedisClient
    class RedisClient {
    
        private config: RedisClientConfig;
    
        constructor(config: RedisClientConfig) {
            this.config = config; // 配置项
        }
    
    	// 获取redis实例
        getConnection(): Promise {
            return new Promise((resolve, reject) => {
                const socket = new RedisSocket(this.config);
    
                socket.on("connect", () => {
                    resolve(socket);
                });
    
                socket.on("error", (err) => {
                    reject(err);
                });
            });
        }
    }
    

    这个类用来建立与服务端的连接,使用getConnection()方法,将会交付一个redisSocket,使用这个Socket可以直接向server发送和接受数据。

    4. 实验

    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()获取到连接实例,并通过这个连接实例设置了两个数据,以及获取了一数据并打印了出来。

    > pnpm dev
    > $6 // 这里的$6你也许会感到奇怪,不过我们很快就会知道这是什么
    > Cookie
    

    5. wireshark 抓包分析


    这一次请求就是一整个完整的TCP流程,
    在这其中TCP保证数据的可靠传输,而RESP(REdis Serialization Protocol)把数据封装成一个fragment段,发送到下面的TCP
    服务端相应的时候也是如此,会把数据封装起来发送到TCP中转发出去。

    看看发送方的RESP

    看看响应的RESP


    所以知道了吗?没错,6其实就是长度那一部分强行转化为字符串的结果,所以在现在很多流行的redis客户端中如ioredis都对RESP报文做了非常完备的解析,这使得开发者能够非常丝滑的与redis服务端交互。(感谢这些开发者做的一切!)

    6. 杂与代码

    Github 仓库

    希望大家都对世界保持好奇!

相关