用Rust实现一个简单的KV Server: day3

279 阅读4分钟

开启掘金成长之旅!这是我参与「掘金日新计划 · 12 月更文挑战」的第12天,点击查看活动详情

前面,我们搭建了一个server\client通讯的基础框架。我们继续在这个框架上面增加命令行解析和键值对存储功能。

Clap解析命令行参数

首先在Cargo.toml文件中加入clap依赖,构建命令行参数:

clap = { version = "4.0.10", features = ["derive"] }

在src目录下新建args.rs文件,新增命令行解析用到的struct和子命令:

use clap::{Parser, Subcommand};

#[derive(Parser)]
#[clap(name = "kv_client")]
pub struct Cli {
    #[clap(subcommand)]
    pub command: ClientArgs,
}

#[derive(Subcommand)]
pub enum ClientArgs {
    Get {
        #[clap(long)]
        key: String,
    },
    Set {
        #[clap(long)]
        key: String,
        #[clap(long)]
        value: String,
    },
    Publish {
        #[clap(long)]
        topic: String,
        #[clap(long)]
        value: String,
    },
    Subscribe {
        #[clap(long)]
        topic: String,
    },
    Unsubscribe {
        #[clap(long)]
        topic: String,
        #[clap(long)]
        id: u32,
    },
}

src/lib.rs中加入对命令行模块进行导出:

mod args;
pub use args::*;

接着对src/bin/kv_client.rs进行改造,加上命令行解析:

use clap::Parser;
use kv_server::{Cli, ClientArgs, ClientConfig, CmdRequest, CmdResponse};

#[tokio::main]
async fn main() -> Result<(), Box<dyn Error>> {
   .....
   //命令行解析
   let client_args = Cli::parse();

   // 解析命令行参数,生成命令
   let cmd = process_args(client_args.command).await?;
   // 命令编码
   cmd.encode(&mut buf).unwrap();
   // 发送命令
   stream.send(buf.freeze()).await.unwrap();
   info!("Send command successed!");
   //用tokio进程处理服务器响应信息
   loop {
       tokio::select! {
           Some(Ok(buf)) = stream.next() => {
               let cmd_res = CmdResponse::decode(&buf[..]).unwrap();
               info!("Receive a response: {:?}", cmd_res);
           }
       }
   }
}

// 生成CmdRequest命令
async fn process_args(client_args: ClientArgs) -> Result<CmdRequest, Box<dyn Error>> {
   match client_args {
       // 生成 GET 命令
       ClientArgs::Get { key } => Ok(CmdRequest::get(key)),
       // 生成 SET 命令
       ClientArgs::Set { key, value } => Ok(CmdRequest::set(key, value.into(), 20000)),
       // 生成 PUBLISH 命令
       ClientArgs::Publish { topic, value } => Ok(CmdRequest::publish(topic, value.into())),
       // 生成 SUBSCRIBE 命令
       ClientArgs::Subscribe { topic } => Ok(CmdRequest::subscribe(topic)),
       // 生成 UNSUBSCRIBE 命令
       ClientArgs::Unsubscribe { topic, id } => Ok(CmdRequest::unsubscribe(topic, id)),
   }
}

打开一个终端,启动kv_sever。打开另一个终端执行以下命令来测试客户端:

cargo run --bin client get --key mykey
cargo run --bin client set --key mykey --value myvalue

服务器和客户端都正常处理了收到的请求和响应。

sled存储数据

首先在Cargo.toml增加依赖:

sled = "0.34.7"

然后创建src/storage目录和src/storage/mod.rs文件,然后在src/lib.rs文件中引入storage模块。

mod storage;
pub use storage::*;

src/storage/mod.rs文件中定义一个storage trait,以便于以后不同存储方式的扩展,代码如下:

pub mod sled_storage;
use bytes::Bytes;
use std::error::Error;

pub trait Storage {
    fn get(&self, key: &str) -> Result<Option<Bytes>, Box<dyn Error>>;
    fn set(&self, key: &str, value: Bytes) -> Result<Option<Bytes>, Box<dyn Error>>;
}

然后,在src/storage目录下创建sled_storage.rs文件。代码如下:

use crate::Storage;
use bytes::Bytes;
use sled::Db;
use std::path::Path;

#[derive(Debug)]
pub struct SledDbStorage(Db);

impl SledDbStorage {
    pub fn new(path: impl AsRef<Path>) -> Self {
        Self(sled::open(path).expect("数据库不存在"))
    }
}

impl Storage for SledDbStorage {
    fn get(&self, key: &str) -> Result<Option<Bytes>, Box<dyn std::error::Error>> {
        let v = self.0.get(key).unwrap().expect("没有值");
        Ok(Some(Bytes::copy_from_slice(v.as_ref())))
    }

    fn set(&self, key: &str, value: Bytes) -> Result<Option<Bytes>, Box<dyn std::error::Error>> {
        self.0.insert(key, value.clone().to_vec());
        Ok(Some(value))
    }
}

Service模块

创建src/service目录,然后创建mod.rs文件及cmd_service.rs文件。在mod.rs文件中加入代码:

pub mod cmd_service;

pub trait CmdService {
    // 解析命令,返回Response
    fn execute(self, store: &impl Storage-> CmdResponse;
}

cmd_service.rs文件中为命令实现CmdService trait,代码如下:

use crate::{CmdResponse, CmdService, Get, Set};

// 为 GET 实现 execute
impl CmdService for Get {
    fn execute(self, store: &impl crate::Storage) -> CmdResponse {
        // 从存储中获取数据,返回CmdResponse
        match store.get(&self.key) {
            Ok(Some(value)) => value.into(),
            Ok(None) => "Not found".into(),
            Err(e) => e.into(),
        }
    }
}

// 为 SET 实现 execute
impl CmdService for Set {
    // 存储数据
    fn execute(self, store: &impl crate::Storage) -> CmdResponse {
        match store.set(&self.key, self.value) {
            Ok(Some(value)) => value.into(),
            Ok(None) => "Set fail".into(),
            Err(e) => e.into(),
        }
    }
}

src/pb/mod.rs中实现从Bytes&strBox<dyn Error>转换为CmdResponse

impl From<Bytes> for CmdResponse {
    fn from(v: Bytes) -> Self {
        Self {
            status: 200u32,
            message: "success".to_string(),
            value: v,
        }
    }
}

impl From<&str> for CmdResponse {
    fn from(s: &str) -> Self {
        Self {
            status: 400u32,
            message: s.to_string(),
            ..Default::default()
        }
    }
}

impl From<Box<dyn Error>> for CmdResponse {
    fn from(e: Box<dyn Error>) -> Self {
        Self {
            status: 500u32,
            message: e.to_string(),
            ..Default::default()
        }
    }
}

然后在src/service/mod.rs中加入service代码:

use crate::{cmd_request::ReqData, sled_storage::SledDbStorage, CmdRequest, CmdResponse, Storage};
use std::sync::Arc;

pub mod cmd_service;

pub trait CmdService {
    // 解析命令,返回Response
    fn execute(self, store: &impl Storage) -> CmdResponse;
}

// 设置默认存储为RocksDB
pub struct Service<S = SledDbStorage> {
    store_svc: Arc<StoreService<S>>,
}

// 在多线程中进行clone
pub struct StoreService<Store> {
    store: Store,
}

impl<Store: Storage> StoreService<Store> {
    pub fn new(store: Store) -> Self {
        Self { store }
    }
}

impl<Store: Storage> Service<Store> {
    pub fn new(store: Store) -> Self {
        Self {
            store_svc: Arc::new(StoreService::new(store)),
        }
    }

    // 执行命令
    pub async fn execute(&self, cmd_req: CmdRequest) -> CmdResponse {
        println!("=== Execute Command Before ===");
        let cmd_res = process_cmd(cmd_req, &self.store_svc.store).await;
        println!("=== Execute Command After ===");
        cmd_res
    }
}

// 实现Clone trait
impl<Store> Clone for Service<Store> {
    fn clone(&self) -> Self {
        Self {
            store_svc: self.store_svc.clone(),
        }
    }
}

// 处理请求命令,返回Response
async fn process_cmd(cmd_req: CmdRequest, store: &impl Storage) -> CmdResponse {
    match cmd_req.req_data {
        // 处理 GET 命令
        Some(ReqData::Get(cmd_get)) => cmd_get.execute(store),
        // 处理 SET 命令
        Some(ReqData::Set(cmd_set)) => cmd_set.execute(store),
        _ => "Invalid command".into(),
    }
}

配置数据库路径

我们修改配置,在conf/server.json中加入sledDB路径

{
  "listen_address": {
    "address": "127.0.0.1:3000"
  },
  "sled_path": {
    "path": "tmp/kvserver"
  }
}

src/config.rs中加入如下代码:

//server端配置
#[derive(Debug, Serialize, Deserialize)]
pub struct ServerConfig {
    pub listen_address: ListenerAddress,
    pub sled_path: SledDbPath,
}

// RocksDB存储目录
#[derive(Debug, Serialize, Deserialize)]
pub struct SledDbPath {
    pub path: String,
}

修改kv_server

kv_server.rs中使用service执行命令,删除process_cmd函数:

初始化Service及存储                                                                          
let service: Service = StoreService::new(SledDbStorage::new(server_config.sled_path.path));
   loop {
        ......
       let svc = service.clone();

       tokio::spawn(async move {
           // 使用Frame的LengthDelimitedCodec进行编解码操作
           let mut stream = Framed::new(stream, LengthDelimitedCodec::new());
           while let Some(Ok(mut buf)) = stream.next().await {
               ......

              // 执行请求命令
               let cmd_res = svc.execute(cmd_req).await;
                ......
            }
            info!("Client {:?} disconnected", addr);\
       });
   }

测试

打开一个终端,运行kv_server

cargo run --bin server

打开另一个终端,运行kv_client,执行set命令,新增键值对,然后关闭终端:

cargo run --bin client set --key mykey --value myvalue

打开另一个终端,运行kv_client,执行get命令,获取键值对:

cargo run --bin client get --key mykey

执行结果:

server端:

Client: 127.0.0.1:63849 connected
  2022-10-08T13:10:18.393685Z  INFO server: Receive a command: CmdRequest { req_data: Some(Get(Get { key: "mykey" })) }
    at src/bin/kv_server.rs:41

=== Execute Command Before ===
=== Execute Command After ===
  2022-10-08T13:10:18.394139Z  INFO server: Client 127.0.0.1:63849 disconnected
    at src/bin/kv_server.rs:50

client端:

  2022-10-08T13:10:18.393603Z  INFO client: Send command successed!
    at src/bin/kv_client.rs:36

  2022-10-08T13:10:18.394190Z  INFO client: Receive a response: CmdResponse { status: 200, message: "success", value: b"myvalue" }
    at src/bin/kv_client.rs:42