开启掘金成长之旅!这是我参与「掘金日新计划 · 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
、&str
、Box<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