用Nodejs 實現一個簡單的 Redis客戶端
0. 寫在前面
大家如果有去看過nodejs所支援的官方庫的話,應該會驚訝於它所提供了非常完善的網路庫,不僅是應用層,傳輸層,等等基礎的協議,我們可以按照事件驅動的邏輯編寫清晰易懂的網路應用,網路服務。這也是本文為什麼選擇Nodejs編寫的原因。
1. 背景映入
大家在使用一些資料庫軟體的時候常常會使用遠程連接
mysql -h xxx.xxx.xxx.xx -u xzzz -p
這裡也指明了ip地址,但是很明顯這裡可不是http協議在服務,而是更加底層的協議 – 傳輸層協議,具體來說是TCP協議(Transmission Control Protocol)。通訊的示意圖如下:
所以很自然的想到,資料庫的客戶端一定經過如下流程,從而與遠程相連接:
身份驗證 –> 運輸層連接建立
運輸層連接建立 –> 客戶端服務端輸入輸出綁定_通道
客戶端服務端輸入輸出綁定_通道 –> 連接中斷
連接中斷 –> 雙方退出釋放資源
所以我們可以嘗試向服務端發送這樣的請求消息,建立與服務端的連接,發送一些數據,接受一些數據,最後斷開連接。
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<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();
}
}
這個類將用來處理建立好後的連接的
- RedisClient
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);
});
});
}
}
這個類用來建立與服務端的連接,使用
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. 雜與程式碼
希望大家都對世界保持好奇!