如何用rust为redis写一个client
最近nosql变得非常流行,redis作为其中的佼佼者,基本也是大部分程序员必备的技能了。我们知道,redis是一个key-value数据库,它运行在内存中但是可以持久化到内存。我们一般很熟悉redis的那些基本命令,但是如何使用某种语言来操作redis呢?那些常用的语言一般都有相应的驱动,但是像rust这样的新语言呢?
Redis Protocol
Redis是通过RESP(REdis Serialization Protocol)来沟通Sever和Client的。这种协议是专门为Redis设计的,它有以下几种特点:
- 实现非常简单
- 解析非常快
- 很容易被人所理解
RESP采用了TCP来通信。一个redis-client通过创建一个到6379端口的TCP连接来和redis-server通信。并且它采用的是Request-Response模型,即每次一个command发送给服务端后,服务端都会返回一个应答给客户端。
RESP协议的语法非常简单,主要就分成以下几种:
- Simple Strings: 第一个字节是"+" 比如
+OK\r\n
- Errors: 第一个字节是"-" 比如
-Error message\r\n
- Integers: 第一个字节是":",比如
:1000\r\n
- Bulk Strings: 第一个字节是"$",后跟一个数字,表示String 的长度,后面再跟字符串。比如字符串xyz,就应该是
$3\r\nxyz\r\n
- Arrays: 第一个字节是"*":*后面跟数组元素个数,再后面跟相应的元素,例如
*2\r\n$3\r\nfoo\r\n$3\r\nbar\r\n
Redis-rust-simple
到目前为止,实际上我们思路已经很清晰了:
- 建立TCP连接
- 生成相应的command并发送
- 接受server发来的reply,并解析
建立连接
这样我们首先创建一个Client结构体
pub struct RedisClient {
io: TcpStream
}
第一步需要建立连接
pub fn new(sock_addr: &str) -> RedisClient {
let tcp_strem = TcpStream::connect(sock_addr).unwrap();
RedisClient {
io : tcp_strem
}
在构造函数里初始化连接
构造命令和发送请求
client是通过tcp发送commands给server的,这个发送过程非常简单,我们只需要往RedisClient结构体的io,即TcpTream里做写操作即可。现在的问题是我如何来构建一个command呢?
从RESP协议中,我们已经知道了构造命令的基础构成。实际上我们只需要将字符,crnl,数字构成分别模块化,最后达成模块化的Simple Strings,Errors,Integers,Bulk Strings,Arrays构造即可。
基础模块
fn add_char(&mut self, s: char) {
self.buf.push(s);
}
fn add_str(&mut self, s: &str) {
self.buf.push_str(s);
}
fn add_uint(&mut self, n: usize) {
self.add_str(n.to_string().as_str());
}
fn add_crnl(&mut self) {
self.add_char('\r');
self.add_char('\n');
}
实际命令构造
// 如果是数组
// For Arrays the first byte of reply is '*'
fn write_arrs(&mut self, n: usize) -> &mut Self {
self.add_char('*');
self.add_uint(n);
self.add_crnl();
self
}
// 如果是字符串
// For Bulk Strings the first byte of the reply is "$"
fn write_buik_string(&mut self, s: &str) -> &mut Self {
if s == "" {
// Null Bulk String
self.add_str("$-1\r\n");
return self
} else {
self.add_char('$');
self.add_uint(s.len());
self.add_crnl();
self.add_str(s);
self.add_crnl();
self
}
}
// For Integers the first byte of the reply is ":"
#[allow(dead_code)]
fn write_int(&mut self, n: usize) -> &mut Self {
self.add_char(':');
self.add_uint(n);
self.add_crnl();
self
}
故而如果我们需要一个set命令,只需要这样做
let mut cmd = CommandWriter::new();
cmd.write_arrs(3)
.write_buik_string("SET")
.write_buik_string(key)
.write_buik_string(val);
同理,如果需要get
let mut cmd = CommandWriter::new();
cmd.write_arrs(2)
.write_buik_string("GET")
.write_buik_string(key);
我们构造完commands后,只需要用write函数向tcptream里写入即可。即
self.io.write(cmd.buf.as_bytes()).unwrap();
接受server的reply并解析
实际上,接收reply这一步是非常简单的,我们只需要用read函数,把tcptream里的数据读到buffer里即可
let mut buffer = [0; 512];
self.io.read(&mut buffer[..]).unwrap();
let response = str::from_utf8(&buffer).unwrap();
通过上面的代码,我们就把server端返回的信息转成一个字符串了,接下来需要做的就是解析字符串,这一步和构造commands正好是一个逆过程,有了之前的经验,我们很容易就能实现代码。
fn parse_io(response: &str) -> Option<RedisResult> {
let vec: Vec<&str> = response.split("\r\n").collect();
// match the first char
match &vec[0][0..1] {
"$" => return Some(RedisResult::RString(vec[1].to_string())),
"*" => {
let len = vec[0][1..].parse::<usize>().unwrap();
let mut v: Vec<String> = Vec::new();
for i in 0..len {
v.push(vec[i + 1].to_string());
}
return Some(RedisResult::RArr(v));
//return None
}
"+" => return Some(RedisResult::RString(vec[1].to_string())),
"-" => panic!(vec[0].to_string()),
_ => return None,
}
}
即先将字符串根据"\r\n"分割开来,然后按照相应的格式解析即可
完整的例子
最后以get命令为例来将前面讲到的这些内容串联起来。对于一个get命令,我们在建立tcp连接后,首先需要构造一个get command,然后将其发送到redis server,redis server接收到命令后,返回一串数据,client端将数据解析最后返回get key获取到的value。代码如下:
pub fn get(mut self, key: &str) -> String {
let mut cmd = CommandWriter::new();
cmd.write_arrs(2)
.write_buik_string("GET")
.write_buik_string(key);
self.io.write(cmd.buf.as_bytes()).unwrap();
self.io.flush().unwrap();
let mut buffer = [0; 512];
self.io.read(&mut buffer[..]).unwrap();
let response = str::from_utf8(&buffer).unwrap();
//println!("{}", response);
let parse = parse_io(response).unwrap();
match parse {
RedisResult::RString(parse) => return parse.to_string(),
_ => panic!("error")
}
}
测试最终结果
我们启动redis-server,然后通过MONITOR来监听redis的状态,
测试代码如下
extern crate redis_simple_rs;
use redis_simple_rs::RedisClient;
fn main() {
let sock_addr: &str = "127.0.0.1:6379";
let mut client = RedisClient::new(sock_addr);
client.set("x", "111");
println!("{}", client.get("x"));
}
即set x的值为111,然后通过get命令,获取key x的值,理论上程序应该会输出111
我们执行cargo run命令,可以看到
至此,一个简单的redis client就完成了,目前只实现了set和get命令,但是有了这两个命令的例子,想添加其他命令只需要依葫芦画瓢即可。项目地址如下redis-simple-rs ,欢迎各位有兴趣的朋友完善。