如何用rust为redis写一个client

如何用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 ,欢迎各位有兴趣的朋友完善。

  • 1
    点赞
  • 3
    收藏
    觉得还不错? 一键收藏
  • 1
    评论

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论 1
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值