如何在rust中使用泛型,trait对象的总结以及kv sever(3)

news2024/11/13 9:56:18

可以说在 Rust 开发中,泛型编程是我们必须掌握的一项技能。在你构建每一个数据结构或者函数时,最好都问问自己:**我是否有必要在此刻就把类型定死?**是不是可以把这个决策延迟到尽可能靠后的时刻,这样可以为未来留有余地?如果我们能通过泛型来推迟决策,系统的架构就可以足够灵活,可以更好地面对未来的变更。

前面学习到trait可以实现参数多态,就是函数或者数据结构用T表示,不是具体类型;
还可以实现特设多态,也就是函数重载,一个函数接口不同的参数有不同的实现;
更牛逼的就是trait做参数的时候,可以实现特征约束,以及多重约束,这体现了组合大于继承的思想,在C++中就是多重继承。KV server的例子就是用实现了store的trait作为泛型参数,实现延迟绑定,同一个数据结构对同一个trait有不同实现,比如kv中以后增加新的存储类型,就会为新的存储类型实现trait,实现就不一样了,在C++中,就是把基类作为参数,相当于把子类赋值给基类,不过这时运行时多态,有运行时虚函数表的开销,而泛型就是静态分发了,运行速度快,但是编译速度慢了

参数是泛型trait
泛型参数的三种使用场景
使用泛型参数延迟数据结构的绑定;
使用泛型参数和 PhantomData,声明数据结构中不直接使用,但在实现过程中需要用到的类型;
使用泛型参数让同一个数据结构对同一个 trait 可以拥有不同的实现。

用泛型参数做延迟绑定
先来看我们已经比较熟悉的,用泛型参数做延迟绑定。kv server中store就是一个泛型参数,这个泛型参数在随后的实现中可以被逐渐约束。只要实现存储trait就可以作为参数。

pub fn dispatch(cmd: CommandRequest, store: &impl Storage) -> CommandResponse { … }
// 等价于
pub fn dispatch<Store: Storage>(cmd: CommandRequest, store: &Store) -> CommandResponse { … }

使用泛型参数和幽灵数据提供额外类型
现在要设计一个 User 和 Product 数据结构,它们都有一个 u64 类型的 id。然而我希望每个数据结构的 id 只能和同种类型的 id 比较,也就是说如果 user.id 和 product.id 比较,编译器就能直接报错,拒绝这种行为。
先用一个自定义的数据结构 Identifier 来表示 id:
pub struct Identifier { inner: u64,}
然后,在 User 和 Product 中,各自用 Identifier 来让 Identifier 和自己的类型绑定,达到让不同类型的 id 无法比较的目的。

#[derive(Debug, Default, PartialEq, Eq)]
pub struct User { id: Identifier<Self>}
#[derive(Debug, Default, PartialEq, Eq)]
pub struct Product { id:Identifier<Self>}

然而它无法编译通过。为什么呢?因为 Identifier 在定义时,并没有使用泛型参数 T,编译器认为 T 是多余的,所以只能把 T 删除掉才能编译通过。但是,删除掉 T,User 和 Product 的 id 就可以比较了,我们就无法实现想要的功能了,怎么办?
PhantomData 中文一般翻译成幽灵数据,这名字透着一股让人不敢亲近的邪魅,但它被广泛用在处理,数据结构定义过程中不需要,但是在实现过程中需要的泛型参数。

#[derive(Debug, Default, PartialEq, Eq)]
pub struct Identifier 
{ inner: u64,
 _tag: PhantomData,}

这样就可以,其实PhantomData 正如其名,它实际上长度为零,是个 ZST(Zero-Sized Type),就像不存在一样,唯一作用就是类型的标记。
比如用户这个结构体,有name,id,以及一个泛型参数T被幽灵数据拥有,代表免费用户还是付费用户。免费用户可以通过订阅方法变成付费用户,into一下.使用 PhantomData 处理这样的状态,可以在编译期做状态的检测,避免运行期检测的负担和潜在的错误。
泛型是静态分发,编译时分配好对应的类型代码。

使用泛型参数来提供多个实现
有时候,对于同一个 trait,我们想要有不同的实现,该怎么办?比如一个方程,它可以是线性方程,也可以是二次方程,我们希望为不同的类型实现不同 Iterator。

#[derive(Debug, Default)]
pub struct Equation 
{ current: u32, 
_method: PhantomData,
}// 线性增长
#[derive(Debug, Default)]
pub struct Linear;// 二次增长
#[derive(Debug, Default)]
pub struct Quadratic;
impl Iterator for Equation
 { type Item = u32;
  fn next(&mut self) -> Option { self.current += 1; if self.current >= u32::MAX { return None; } Some(self.current) }}
  impl Iterator for Equation
   {
   type Item = u32; 
   fn next(&mut self) -> Option { self.current += 1; if self.current >= u16::MAX as u32 { return None; } Some(self.current * self.current) }}

这样做有什么好处么?为什么不构建两个数据结构 LinearEquation 和 QuadraticEquation,分别实现 Iterator 呢?
的确,对于这个例子,使用泛型的意义并不大,因为 Equation 自身没有很多共享的代码。但如果 Equation,只除了实现 Iterator 的逻辑不一样,其它大量的代码都是相同的,并且未来除了一次方程和二次方程,还会支持三次、四次……,那么,用泛型数据结构来统一相同的逻辑,用泛型参数的具体类型来处理变化的逻辑,就非常有必要了。
类似于C++中继承的意思,Equation是基类,有iterator这个方法,LinearEquation 和 QuadraticEquation是子类,具体实现iterator。

额外的情况:
返回值携带泛型参数怎么办?
比如,对于 get_iter() 方法,并不关心返回值是一个什么样的 Iterator,只要它能够允许我们不断调用 next() 方法,获得一个 Kvpair 的结构,就可以了。
Rust 目前还不支持在 trait 里使用 impl trait 做返回值。那怎么办?很简单,我们可以返回 trait object,它消除了类型的差异,把所有不同的实现 Iterator 的类型都统一到一个相同的 trait object 下

pub trait Storage {
    ...
    /// 遍历 HashTable,返回 kv pair 的 Iterator
    fn get_iter(&self, table: &str) -> 
        Result<Box<dyn Iterator<Item = Kvpair>>, KvError>;
}

当然,泛型编程也是一把双刃剑。任何时候,当我们引入抽象,即便能做到零成本抽象,要记得抽象本身也是一种成本。当我们把代码抽象成函数、把数据结构抽象成泛型结构,即便运行时几乎并无添加额外成本,它还是会带来设计时的成本,如果抽象得不好,还会带来更大的维护上的成本

trait object 是如何在实战中使用
在这里插入图片描述
这个例子表示,特征对象就是用&dyn trait的方式,把实际类型表示为实现了trait的上层类型,和C++类似,有一个table,里面是具体类型对于trait的实现。相当于多态,把子类退化成基类,只不过基类有个table虚函数表,存储了实际类型对于trait的实现。
这里的疑问就是,前面说过,trait做参数就可以实现特设多态,达到类似于传入泛型参数,而根据不同实际类型,实现不同。为什么需要特征对象呢?
1.主要是为了编程方便,逻辑性和代码可读性好。举个例子,如果我们想要实现一个UI组件,有不同的元素(按钮,文本框等等,这些组件都有draw方法)。都存在一个表里vec,需要使用同一个方法逐一渲染在屏幕上!如果用泛型特征约束的话,那么列表中必须都是同一种类型。为了解决上面的所有问题,Rust 引入了一个概念 —— 特征对象。Box实现。任何实现了 Draw 特征的类型,都可以存放在vec中。
trait对象可以让代码变的简洁,因为实现的时候就不要带着泛型参数了。
2.第二就是在一些需要泛型返回值的时候。Rust 目前还不支持在 trait 里使用 impl trait 做返回值。那怎么办?很简单,我们可以返回 trait object。比如返回迭代器时,把所有不同的实现 Iterator 的类型都统一到一个相同的 trait object 下。

总结一下就是:
当系统需要使用多态来解决复杂多变的需求,让同一个接口可以展现不同的行为时,我们要决定究竟是编译时的静态分发更好(泛型参数),还是运行时的动态分发更好(特征对象)。
动态分发会有运行时开销,但是会使代码更加简洁,特别是当需要对不同具体类型做一个抽象,比如把不同组件形成UI组件集合,就需要特征对象,因为数组只能放相同类型的元素,可以考虑元组?以及返回值含有泛型时必须用特征对象,因为rust暂不支持impl trait作为返回值。
静态分发零成本抽象,但是设计成本是有的,简单的可以用,如果太复杂了,涉及多个trait,联系很复杂,就可以不考虑。

tips:
我们知道& dyn draw, Box, , Arc都是可以做特征对象的。但是动态分发额外开销对于静态分发到底多多少?
如果是& dyn draw(分配在栈上的ptr和vptr),其实就是多一次vtable的内存访问而已,影响并不大。而 Box, , Arc因为会多一次堆内存的分配,这个影响很大,导致速度慢几十倍。
所以,大部分情况,我们在撰写代码的时候,不必太在意 trait object 的性能问题。如果你实在在意关键路径上 trait object 的性能,那么先尝试看能不能不要做额外的堆内存分配。
(对于作为返回值(栈对象会在函数结束销毁),以及线程间传递(必须要实现send trait),必须要用Box, , Arc)

如何围绕trait来设计和架构系统?
其实不光是 Rust 中的 trait,任何一门语言,和接口处理相关的概念,都是那门语言在使用过程中最重要的概念。软件开发的整个行为,基本上可以说是不断创建和迭代接口,然后在这些接口上进行实现的过程。

构建一个简单的KV server-高级trait技巧
我们已经完成了 KV store 的基本功能,但留了两个小尾巴:
Storage trait 的 get_iter() 方法没有实现;
Service 的 execute() 方法里面还有一些 TODO,需要处理事件的通知。
完成持久化存储


impl Storage for MemTable {
    ...
    fn get_iter(&self, table: &str) -> Result<Box<dyn Iterator<Item = Kvpair>>, KvError> {
        // 使用 clone() 来获取 table 的 snapshot
        let table = self.get_or_create_table(table).clone();
        let iter = table
            .iter()
            .map(|v| Kvpair::new(v.key(), v.value().clone()));
        Ok(Box::new(iter)) // <-- 编译出错
    }
}

table.iter() 使用了 table 的引用,我们返回 iter,但 iter 引用了作为局部变量的 table,所以无法编译通过。此刻,我们需要有一个能够完全占有 table 的迭代器,我们可以用 table.into_iter() 把 table 的所有权转移给 iter:let iter = table.into_iter().map(|data| data.into());
(String, Value) 需要转换成 Kvpair,我们依旧用 into() 来完成这件事。
我们还是有必要思考一下,如果以后想为更多的 data store 实现 Storage trait,都会怎样处理 get_iter() 方法?

我们会:
拿到一个关于某个 table 下的拥有所有权的 Iterator
对 Iterator 做 map
将 map 出来的每个 item 转换成 Kvpair

这里的第 2 步对于每个 Storage trait 的 get_iter() 方法的实现来说,都是相同的。有没有可能把它封装起来呢,也就是实现一个storeiterator trait里面包含了map操作,这样每当其他存储类型要实现get_iter,第二步就可以省略。的确,在这个 KV server 的例子里,这样的抽象收益不大。但是,如果刚才那个步骤不是 3 步,而是 5 步 /10 步,其中大量的步骤都是相同的,也就是说,我们每实现一个新的 store,就要撰写相同的代码逻辑,那么,这个抽象就非常有必要了。

支持事件通知


pub fn execute(&self, cmd: CommandRequest) -> CommandResponse {
    debug!("Got request: {:?}", cmd);
    // TODO: 发送 on_received 事件
    let res = dispatch(cmd, &self.inner.store);
    debug!("Executed response: {:?}", res);
    // TODO: 发送 on_executed 事件

    res
}

为了解决这些 TODO,我们需要提供事件通知的机制:在创建 Service 时,注册相应的事件处理函数;在 execute() 方法执行时,做相应的事件通知,使得注册的事件处理函数可以得到执行。
先看事件处理函数如何注册。
如果想要能够注册,那么倒推也就是,Service/ServiceInner 数据结构就需要有地方能够承载事件注册函数。可以尝试着把它加在 ServiceInner 结构里:


/// Service 内部数据结构
pub struct ServiceInner<Store> {
    store: Store,
    on_received: Vec<fn(&CommandRequest)>,
    on_executed: Vec<fn(&CommandResponse)>,
    on_before_send: Vec<fn(&mut CommandResponse)>,
    on_after_send: Vec<fn()>,
}

在撰写事件注册的代码之前,还是先写个测试,从使用者的角度,考虑如何进行注册


#[test]
fn event_registration_should_work() {
    fn b(cmd: &CommandRequest) {
        info!("Got {:?}", cmd);
    }
    fn c(res: &CommandResponse) {
        info!("{:?}", res);
    }
    fn d(res: &mut CommandResponse) {
        res.status = StatusCode::CREATED.as_u16() as _;
    }
    fn e() {
        info!("Data is sent");
    }

    let service: Service = ServiceInner::new(MemTable::default())
        .fn_received(|_: &CommandRequest| {})
        .fn_received(b)
        .fn_executed(c)
        .fn_before_send(d)
        .fn_after_send(e)
        .into();

    let res = service.execute(CommandRequest::new_hset("t1", "k1", "v1".into()));
    assert_eq!(res.status, StatusCode::CREATED.as_u16() as _);
    assert_eq!(res.message, "");
    assert_eq!(res.values, vec![Value::default()]);
}

从测试代码中可以看到,我们希望通过 ServiceInner 结构,不断调用 fn_xxx 方法,为 ServiceInner 注册相应的事件处理函数;添加完毕后,通过 into() 方法,我们再把 ServiceInner 转换成 Service。这是一个经典的构造者模式(Builder Pattern),在很多 Rust 代码中,都能看到它的身影。(构造复杂对象,只需要把参数传进去,给构造类,具体怎么构造不用管。使用private修饰构造方法,外界无法直接创建对象,只能通过内部Builder类的build方法;隐藏了构建对象的过程;暴露出给外部调用的方法,用于构造组件。)


impl<Store: Storage> ServiceInner<Store> {
    pub fn new(store: Store) -> Self {
        Self {
            store,
            on_received: Vec::new(),
            on_executed: Vec::new(),
            on_before_send: Vec::new(),
            on_after_send: Vec::new(),
        }
    }

    pub fn fn_received(mut self, f: fn(&CommandRequest)) -> Self {
        self.on_received.push(f);
        self
    }

    pub fn fn_executed(mut self, f: fn(&CommandResponse)) -> Self {
        self.on_executed.push(f);
        self
    }

    pub fn fn_before_send(mut self, f: fn(&mut CommandResponse)) -> Self {
        self.on_before_send.push(f);
        self
    }

    pub fn fn_after_send(mut self, f: fn()) -> Self {
        self.on_after_send.push(f);
        self
    }
}

我们虽然完成了事件处理函数的注册,但现在还没有发事件通知。另外因为我们的事件包括不可变事件(比如 on_received)和可变事件(比如 on_before_send),所以事件通知需要把二者分开。来定义两个 trait:Notify 和 NotifyMut:

/// 事件通知(不可变事件)
pub trait Notify<Arg> {
    fn notify(&self, arg: &Arg);
}

/// 事件通知(可变事件)
pub trait NotifyMut<Arg> {
    fn notify(&self, arg: &mut Arg);
}



impl<Arg> Notify<Arg> for Vec<fn(&Arg)> {
    #[inline]
    fn notify(&self, arg: &Arg) {
        for f in self {
            f(arg)
        }
    }
}

impl<Arg> NotifyMut<Arg> for Vec<fn(&mut Arg)> {
  #[inline]
    fn notify(&self, arg: &mut Arg) {
        for f in self {
            f(arg)
        }
    }
}

其中的 Arg 参数,对应事件注册函数里的 arg,比如:fn(&CommandRequest);

Notify / NotifyMut trait 实现好之后,我们就可以修改 execute() 方法了:


impl<Store: Storage> Service<Store> {
    pub fn execute(&self, cmd: CommandRequest) -> CommandResponse {
        debug!("Got request: {:?}", cmd);
        self.inner.on_received.notify(&cmd);
        let mut res = dispatch(cmd, &self.inner.store);
        debug!("Executed response: {:?}", res);
        self.inner.on_executed.notify(&res);
        self.inner.on_before_send.notify(&mut res);
        if !self.inner.on_before_send.is_empty() {
            debug!("Modified response: {:?}", res);
        }

        res
    }
}

现在,相应的事件就可以被通知到相应的处理函数中了。这个通知机制目前还是同步的函数调用,未来如果需要,我们可以将其改成消息传递,进行异步处理。
只能、整体过程就是在service结构中加入响应事件的数组,数组元素是要执行的函数,因为是数组,所以在构造service可以利用构造者模式,不断调用 fn_xxx 方法,这个方法就是把要执行的函数加入相应的响应事件的数组。然后执行到某个地方notify一下,执行数组里面的函数,进行通知。
注意,这里为什么采用为某个触发点采用数组的形式,也是为了满足开闭原则,因为比如收到请求后,要做的事或者说通知随着需求是在变化的,可能增加或者减少,如果只采用一个函数的形式,直接fn_xxx里面执行对应实现,面对变化就要修改代码,不满足开闭原则。

现在想想我们整个项目满足开闭原则地方有哪些:
1、使用了泛型trait,实现了C++中类似多态的效果,以后新增存储类型不需要修改代码,只要为新增的类型实现trait即可。并且泛型是静态分发,零成本抽象;
2、对于get_iter方法,返回参数使用的是 特征对象,不在乎具体类型,只要满足实现了iterator trait就可以,也是实现类似于多态的效果,是动态分发;另外,我们分析不同类型的迭代器第二步都是对 Iterator 做 map,所以我们把这一步封装,实现了
store iterator,这样当有新的存储类型,就不用重复写第二步了,特别是当这种共同的操作有很多步时,意义更大。
3.这里的事件通知机制,不是单独一个处理事件的函数,因为处理过程可能变化,所以设置了处理数组,构造时可以利用构造者模式不断调用fn_xxx往处理数组中添加函数,需要通知的时候notify顺序执行数组中的函数。
4、测试store部分的测试代码也符合开闭原则,接口是稳定的,用泛型trait作为接口参数,当有新增存储类型测试时,不需要修改代码了。

为持久化数据库实现 Storage trait
到目前为止,我们的 KV store 还都是一个在内存中的 KV store。一旦终止应用程序,用户存储的所有 key / value 都会消失。我们希望存储能够持久化。
一个方案是为 MemTable 添加 WAL 和 disk snapshot 支持,让用户发送的所有涉及更新的命令都按顺序存储在磁盘上,同时定期做 snapshot,便于数据的快速恢复;
另一个方案是使用已有的 KV store,比如 RocksDB,或者 sled。RocksDB 是 Facebook 在 Google 的 levelDB 基础上开发的嵌入式 KV store,用 C++ 编写,而 sled 是 Rust 社区里涌现的优秀的 KV store,对标 RocksDB。二者功能很类似,从演示的角度,sled 使用起来更简单,更加适合今天的内容,如果在生产环境中使用,RocksDB 更加合适,因为它在各种复杂的生产环境中经历了千锤百炼。所以,我们今天就尝试为 sled 实现 Storage trait,让它能够适配我们的 KV server。
首先在 Cargo.toml 里引入 sled:sled = “0.34” # sled db
然后创建 src/storage/sleddb.rs,并添加如下代码:


use sled::{Db, IVec};
use std::{convert::TryInto, path::Path, str};

use crate::{KvError, Kvpair, Storage, StorageIter, Value};

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

impl SledDb {
    pub fn new(path: impl AsRef<Path>) -> Self {
        Self(sled::open(path).unwrap())
    }

    // 在 sleddb 里,因为它可以 scan_prefix,我们用 prefix
    // 来模拟一个 table。当然,还可以用其它方案。
    fn get_full_key(table: &str, key: &str) -> String {
        format!("{}:{}", table, key)
    }

    // 遍历 table 的 key 时,我们直接把 prefix: 当成 table
    fn get_table_prefix(table: &str) -> String {
        format!("{}:", table)
    }
}

/// 把 Option<Result<T, E>> flip 成 Result<Option<T>, E>
/// 从这个函数里,你可以看到函数式编程的优雅
fn flip<T, E>(x: Option<Result<T, E>>) -> Result<Option<T>, E> {
    x.map_or(Ok(None), |v| v.map(Some))
}

impl Storage for SledDb {
    fn get(&self, table: &str, key: &str) -> Result<Option<Value>, KvError> {
        let name = SledDb::get_full_key(table, key);
        let result = self.0.get(name.as_bytes())?.map(|v| v.as_ref().try_into());
        flip(result)
    }

    fn set(&self, table: &str, key: String, value: Value) -> Result<Option<Value>, KvError> {
        let name = SledDb::get_full_key(table, &key);
        let data: Vec<u8> = value.try_into()?;

        let result = self.0.insert(name, data)?.map(|v| v.as_ref().try_into());
        flip(result)
    }

    fn contains(&self, table: &str, key: &str) -> Result<bool, KvError> {
        let name = SledDb::get_full_key(table, &key);

        Ok(self.0.contains_key(name)?)
    }

    fn del(&self, table: &str, key: &str) -> Result<Option<Value>, KvError> {
        let name = SledDb::get_full_key(table, &key);

        let result = self.0.remove(name)?.map(|v| v.as_ref().try_into());
        flip(result)
    }

    fn get_all(&self, table: &str) -> Result<Vec<Kvpair>, KvError> {
        let prefix = SledDb::get_table_prefix(table);
        let result = self.0.scan_prefix(prefix).map(|v| v.into()).collect();

        Ok(result)
    }

    fn get_iter(&self, table: &str) -> Result<Box<dyn Iterator<Item = Kvpair>>, KvError> {
        let prefix = SledDb::get_table_prefix(table);
        let iter = StorageIter::new(self.0.scan_prefix(prefix));
        Ok(Box::new(iter))
    }
}

impl From<Result<(IVec, IVec), sled::Error>> for Kvpair {
    fn from(v: Result<(IVec, IVec), sled::Error>) -> Self {
        match v {
            Ok((k, v)) => match v.as_ref().try_into() {
                Ok(v) => Kvpair::new(ivec_to_key(k.as_ref()), v),
                Err(_) => Kvpair::default(),
            },
            _ => Kvpair::default(),
        }
    }
}

fn ivec_to_key(ivec: &[u8]) -> &str {
    let s = str::from_utf8(ivec).unwrap();
    let mut iter = s.split(":");
    iter.next();
    iter.next().unwrap()
}

这段代码主要就是在实现 Storage trait。每个方法都很简单,就是在 sled 提供的功能上增加了一次封装。
而测试代码就可以复用之前的,这里也体现了测试代码的开闭原则,测试的接口稳定,实现可以变。


mod sleddb;

pub use sleddb::SledDb;

#[cfg(test)]
mod tests {
    use tempfile::tempdir;

    use super::*;

    ...

    #[test]
    fn sleddb_basic_interface_should_work() {
        let dir = tempdir().unwrap();
        let store = SledDb::new(dir);
        test_basi_interface(store);
    }

    #[test]
    fn sleddb_get_all_should_work() {
        let dir = tempdir().unwrap();
        let store = SledDb::new(dir);
        test_get_all(store);
    }

    #[test]
    fn sleddb_iter_should_work() {
        let dir = tempdir().unwrap();
        let store = SledDb::new(dir);
        test_get_iter(store);
    }
}

最后实际运行的整体测试代码,可以看到,主函数几乎没怎么修改,就是在构造service修改了具体的存储类型,利用了构造者模式不断调用fn_xxx方法来把通知的函数push到事件数组中,执行的时候会调用notify函数执行。
并且,如果要新增通知事件,只需要构造时多调用一次fn_xxx函数即可,不用修改代码;如果要新增存储类型,也是为其实现store trait即可,不需要修改代码,execute会自动调用相应的函数去处理。(涉及到protobuf文件序列化反序列化,相应命令的执行函数也就是存储读取逻辑)


use anyhow::Result;
use async_prost::AsyncProstStream;
use futures::prelude::*;
use kv1::{CommandRequest, CommandResponse, Service, ServiceInner, SledDb};
use tokio::net::TcpListener;
use tracing::info;

#[tokio::main]
async fn main() -> Result<()> {
    tracing_subscriber::fmt::init();
    let service: Service<SledDb> = ServiceInner::new(SledDb::new("/tmp/kvserver"))
        .fn_before_send(|res| match res.message.as_ref() {
            "" => res.message = "altered. Original message is empty.".into(),
            s => res.message = format!("altered: {}", s),
        })
        .into();
    let addr = "127.0.0.1:9527";
    let listener = TcpListener::bind(addr).await?;
    info!("Start listening on {}", addr);
    loop {
        let (stream, addr) = listener.accept().await?;
        info!("Client {:?} connected", addr);
        let svc = service.clone();
        tokio::spawn(async move {
            let mut stream =
                AsyncProstStream::<_, CommandRequest, CommandResponse, _>::from(stream).for_async();
            while let Some(Ok(cmd)) = stream.next().await {
                info!("Got a new command: {:?}", cmd);
                let res = svc.execute(cmd);
                stream.send(res).await.unwrap();
            }
            info!("Client {:?} disconnected", addr);
        });
    }
}

本文来自互联网用户投稿,该文观点仅代表作者本人,不代表本站立场。本站仅提供信息存储空间服务,不拥有所有权,不承担相关法律责任。如若转载,请注明出处:http://www.coloradmin.cn/o/417232.html

如若内容造成侵权/违法违规/事实不符,请联系多彩编程网进行投诉反馈,一经查实,立即删除!

相关文章

谷歌的Bard和OpenAI的GPT4的对比

前言 随着上个月21日谷歌面向公众开放人工智能聊天机器人Bard的访问权限&#xff0c;同样是上个月的14日OpenAI为聊天机器人ChatGPT发布的最新语言模型&#xff1a;GPT-4的问世&#xff0c;可以说关于ChatGPT应用的推出进入了百家争鸣的情况&#xff0c;而且竞争变得激烈起来&a…

手把手教你Temporal Fusion Transformer——Pytorch实战

建立了一个关于能源需求预测的端到端项目&#xff1a; 如何为 TFT 格式准备我们的数据。 如何构建、训练和评估 TFT 模型。 如何获取对验证数据和样本外预测的预测。 如何使用built-in model的可解释注意力机制计算特征重要性、季节性模式和极端事件鲁棒性。 什么是Temporal F…

Prophet学习(二) 时序预测开源工具包Prophet介绍

目录 一、Prophet 简介 二、Prophet 适用场景 三、Prophet 算法的输入输出 四、Prophet 算法原理 五、与机器学习算法的对比 六、代码 6.1 依赖安装 6.2 预测demo 七、参考资料 八、官方链接&#xff1a; 九、案例链接&#xff1a; 一、Prophet 简介 Prophet是Faceb…

C++学习 Day1

目录 1. C关键字(C98) 2.命名空间 3. C输入&输出 1. C关键字(C98) C总计63个关键字&#xff0c;C语言32个关键字 目前只是初学阶段&#xff0c;只是大致的了解&#xff0c;以后再深入研究。 2.命名空间 在C/C中&#xff0c;变量、函数和后面要学到的类都是大量存在的&am…

C++中的类模版

&#x1f436;博主主页&#xff1a;ᰔᩚ. 一怀明月ꦿ ❤️‍&#x1f525;专栏系列&#xff1a;线性代数&#xff0c;C初学者入门训练&#xff0c;题解C&#xff0c;C的使用文章&#xff0c;「初学」C &#x1f525;座右铭&#xff1a;“不要等到什么都没有了&#xff0c;才下…

Linux驱动开发——高级I/O操作(一)

一个设备除了能通过读写操作来收发数据或返回、保存数据&#xff0c;还应该有很多其他的操作。比如一个串口设备还应该具备波特率获取和设置、帧格式获取和设置的操作;一个LED设备甚至不应该有读写操作&#xff0c;而应该具备点灯和灭灯的操作。硬件设备是如此众多&#xff0c;…

PDF怎么转CAD文件?(免费!高效转换方法汇总)

一般而言&#xff0c;PDF图纸是不能修改的。若需修改&#xff0c;则需将PDF转CAD&#xff0c;此时如何满足PDF转CAD的需求呢&#xff1f;今天&#xff0c;我将教你两种免费的PDF转CAD的方法&#xff0c;助力高效办公。 1.本地软件转换法 这是用本地软件转换方法&#xff0c;支…

【系统集成项目管理工程师】项目管理一般知识

&#x1f4a5;项目管理一般知识 一、什么是项目 1、项目定义 项目是为达到特定的目的&#xff0c;使用一定资源&#xff0c;在确定的期间内&#xff0c;为特定发起人提供独特的产品、服务或成果而进行的一系列相互关联的活动的集合。项目有完整的生命周期&#xff0c;有开始…

Dubbo(超级无敌认真好用,万字收藏篇!!!!)

文章目录Dubbo前言大型互联网架构目标集群和分布式集群分布式架构演进1 Dubbo概述1.1 Dubbo概念1.2 Dubbo架构图2 Dubbo快速入门2.1 Zookeeper的安装2.2 springBoot整合DubboZookeeper2.2.1 创建项目Dubbo--provider2.2.2 创建项目Dubbo--consumer2.2.3 测试3 Dubbo高级特性3.1…

可视化 | Flask+Pyecharts可视化模板

文章目录&#x1f3f3;️‍&#x1f308; 1. 系统说明界面&#x1f3f3;️‍&#x1f308; 2. 柱状图示例界面&#x1f3f3;️‍&#x1f308; 3. 饼状图示例界面&#x1f3f3;️‍&#x1f308; 4. 折现图示例界面&#x1f3f3;️‍&#x1f308; 5. 散点图示例界面&#x1f3…

人工智能(Pytorch)搭建transformer模型,真正跑通transformer模型,深刻了解transformer的架构

大家好&#xff0c;我是微学AI&#xff0c;今天给大家讲述一下人工智能(Pytorch)搭建transformer模型&#xff0c;手动搭建transformer模型&#xff0c;我们知道transformer模型是相对复杂的模型&#xff0c;它是一种利用自注意力机制进行序列建模的深度学习模型。相较于 RNN 和…

【数据结构Java】--图、BFS、DFS、拓扑结构

目录 一、图&#xff08;Graph&#xff09; 1.概念 2.有向图 3.出度、入度 4.无向图 5.简单图、多重图 6.无向完全图 7.有向完全图 8.有权图 9.连通图 10.连通分量&#xff08;无向图&#xff09; 11.强连通图&#xff08;有向图&#xff09; 12.强连通分量 13.邻接矩…

微服务架构-服务网关(Gateway)-权限认证(分布式session替代方案)

权限认证-分布式session替代方案 前面我们了解了Gateway组件的过滤器&#xff0c;这一节我们就探讨一下Gateway在分布式环境中的一个具体用例-用户鉴权。 1、传统单应用的用户鉴权 从我们开始学JavaEE的时候&#xff0c;就被洗脑式灌输了一种权限验证的标准做法&#xff0c;…

Adobe全新AI工具引关注,Adobe firefly助力创作更高效、更有创意

原标题&#xff1a;Adobe全新AI工具引关注&#xff0c;Adobe firefly&#xff08;萤火虫&#xff09;助力创作更高效、更有创意。 以ChatGPT为首的生成式AI、AIGC等工具的战局正如火如荼的进行中..... 除了微软、百度的聊天机器人和一些初创公司的AI画图工具令人惊艳&#xff…

Greenplum数据库执行器——PartitionSelector执行节点

为了能够对分区表有优异的处理能力&#xff0c;对于查询优化系统来说一个最基本的能力就是做分区裁剪partition pruning&#xff0c;将query中并不涉及的分区提前排除掉。如下执行计划所示&#xff0c;由于单表谓词在parititon key上&#xff0c;在优化期间即可确定哪些可以分区…

003:Mapbox GL设定不同的投影方式

第003个 点击查看专栏目录 本示例的目的是介绍演示如何在vue+mapbox中设定不同的投影方式 。默认情况下为Mercator投影,或者设置为null或者undefined时候,显示为Mercator投影。 直接复制下面的 vue+mapbox源代码,操作2分钟即可运行实现效果 文章目录 示例效果配置方式示例源…

【分享】维格表集成易聊实现线索自动化,减少流失率

公司•介绍 北京某职业教育公司专注行业发展、国际就业、留学、移民咨询。秉承专业性至上的原则&#xff0c;与行业内专家、高等学府以及产业集团合作&#xff0c;并邀请各领域专家组建了强大的专委会团队&#xff0c;为公司的业务开展提供专业性支持。 客户•遇到的问题 作为…

【Java面试八股文宝典之MySQL篇】备战2023 查缺补漏 你越早准备 越早成功!!!——Day23

大家好&#xff0c;我是陶然同学&#xff0c;软件工程大三即将实习。认识我的朋友们知道&#xff0c;我是科班出身&#xff0c;学的还行&#xff0c;但是对面试掌握不够&#xff0c;所以我将用这100多天更新Java面试题&#x1f643;&#x1f643;。 不敢苟同&#xff0c;相信大…

用Spring Doc代替Swagger

1 OpenApi OpenApi 是一个业界的 API 文档标准&#xff0c;是一个规范&#xff0c;这个规范目前有两大实现&#xff0c;分别是&#xff1a; SpringFoxSpringDoc 其中 SpringFox 其实也就是我们之前所说的 Swagger&#xff0c;SpringDoc 则是我们今天要说的内容。 OpenApi 就…

苹果智能戒指专利曝光,Find My技术加持不易丢

根据美国商标和专利局&#xff08;USPTO&#xff09;公示的清单&#xff0c;苹果近日获得了一项“智能戒指”相关的设计专利&#xff0c;编号为“US 11625098 B2”。 这款智能戒指专利主要服务于增强现实&#xff08;AR&#xff09;或者虚拟现实&#xff08;VR&#xff09;场…