Netplus试图解决什么问题

写代码的冰冰

共 4562字,需浏览 10分钟

 ·

2021-04-18 19:59


本文为初见,Netplus快速开始之PingPong Example系列第二篇


1. 背景

网络信息传递中的两个基本问题,即建立连接,传递消息,都已被各操作系统解决好了,各家的技术细节并不一样,暴露出来的接口虽有相似之处,但仍然难以做到一份代码,到处运行。一些代码库,如libuv, libevent,在系统兼容以及IO事件通知的上面做得非常优秀,然,从实际应用开发者的角度来看,它们仍然遗留了很多需要开发者自己去解决的问题,如跨平台的一些细节、IO事件处理、线程安全及性能调优、应用层协议的解析。而这些问题都涉及更多的知识以及编程经验,这就是门槛啊,任何试图降低门槛的努力都是有价值的,也肯定是值得的。

为降低门槛,Netplus在很多方面都做尝试。

2. Netplus为降低门槛进行的尝试

2.1 简化对象管理

对象管理是个古老的话题,各语言在这方面都做得相当出色,尤其是Java,需要的时候,我们new一个,不用的时候,直接置null, 这种简单直接的使用方式,着实迷人,c++是否也能如此呢?

c++在这方面也是努力,stl中有如下方案:

  • std::auto_ptr

  • std::unique_ptr

  • std::shared_ptr

  • std::weak_ptr

  • std::enable_shared_from_this

已经有这么多解决方案了,还没有解决好问题吗?为啥,Netplus又造轮子 ?

简单列一下理由如下:

  • 并没有解决好线程安全的问题。

  • shared_ptr使用不当会造成二次delete,最典型的当属如这样的代码,int* p=new int; std::shared_ptr<int>(p); std::shared_ptr<int>(p)。虽然这是一个错误的使用案例,但是我相信写过这样代码的人应该不少。

  • 有多种方式获取到raw_pointer,有了raw_pointer。特别是周期长,不段有新人加入的项目,鬼晓得会有人拿这个raw_pointer来干什么用(最好你不明白我在说什么 )。

  • sizeof(std::shared_ptr<T>) == sizeof(T*)*2,理想的期望当然是如sizeof(netp::ref_ptr<T>) == sizeof(T*)

  • std::shared_ptr<T>对象在创建的时候,还会额外new一个对象来存control_block, 这对于有大量小对象的系统来说,性能会受到不少的影响。

  • 工具太多,眼花缭乱,新人会有一些心理负担。

我说了列一下的,没想到一直子列了这么多,就点到为止吧。

注:我列的这些问题,并不是想说std::shared_ptr有多糟糕,除此之外,std::shared_ptr也是有很多好处的,比如,它是非侵入式的,除了托管对象,它还能托管数组,因可自定义deleter,使得它还可以托管一些其它资源,如file descriptor,等system handle。造成今天这样的局面,有一些是历史遗留问题,还有一些就是选择。

Netplus其中一个重要的目标是降低门槛,因此,易用,够用,简洁成为其第一要考虑的问题,综合考虑之后,Netplus选择了侵入式的引用计数方式,它具备如下性质:

  • 阻止显式new/delete (编译器会报错)

  • sizeof(netp::ref_ptr<T>) == sizeof(T*)

  • 小对象友好

  • 线程安全

通过这些努力,我们达成了下面这样的小目标

  • 想要的时候make_ref<T>(...)

  • 不要的时候,直接置null

详细的关于Netplus智能指针,请参:

Concept: Smart Pointergithub.com


2.2 形式上消灭回调函数

先看下面代码:

struct foo{};
struct bar{};
struct callback_ctx_t {
foo* foo_ptr;
bar* bar_ptr;
};
typedef void (*callback_t)(int v, callback_ctx_t* ctx);
void do_async( callback_t cb, callback_ctx_t* ctx ){
xx_cb = cb;
xx_ctx = ctx;
// do ..
// if the expected event happens in the future, we must call xx_cb(event_result, ctx )
}
//this func would be called in future, if event happens
void do_if_event_ready(){
int v=8;//set event result value as 8 for simplicity
xx_cb(v,ctx);
}

void callback_impl(int v, callback_ctx_t* ctx) {
if(v == 8) {
//do...
}
}
int main(int argc, char** argv) {
callback_ctx_t* ctx = new callback_ctx_t;
ctx->foo_ptr = new foo();
ctx->bar_ptr = new bar();
do_async(callback_impl, ctx);
for(;;){}
}

这是一个典型的回调案例。

do_async调用后,它的结果要等到某event发生之后才会有。于是我们传入一个函数,这个函数主要是用来接收将来才会知道的那个值,同时,为了能在回调函数里面执行相关的操作,我们可能还要传入一个ctx,在ctx里面保存需要的上下文。

回调虽然能解决问题,但是,它不便于代码阅读,不便于直观的逻辑表达,更不便于资源管理,工程中的很多问题都与回调有关。

我们能否有更好的表达方式呢?

我们能不能在do_async返回一个对象,这个对象可以直接取将来的值,值的类型是int?这样不就没有回调了吗?

于是,经过精心设计,代码变成下面这样:

netp::ref_ptr<netp::promise<int>> do_async(){
xx_promise = netp::make_ref<netp::promise<int>>():
// do ..
return xx_promise;
}

//this func would be called in future, if event happens
void do_if_event_ready(){
int v=8;//set event result value as 8 for simplicity
xx_promsie->set(v);
}
struct foo{};
struct bar{};
int main(int argc, char** argv) {
foo* foo_ptr = new foo();
bar* bar_ptr = new bar();
netp::ref_ptr<netp::promise<int>> int_p = do_async();
int_p ->if_done([foo_ptr,bar_ptr](int v){
//this lambda would be called once the event ready
if(v == 8) {
//do ...
}
});
for(;;){}
}

有没有发现点什么呢?

我们在形式上消灭了回调,整个代码的表达,有没有感觉更直观一点?请注意if_done这一行,有发现什么了吗,仔细体会,带着如下问题仔细体会?

  • 线程调度的本质

  • 协程的本质

  • 任务调度的本质

  • 任务与线程,与CPU资源是何关系,如何最大化cpu利用率?


Promise的一小步,表达上的一大步。

结合c++11的lambda,终于,异步的代码看起来,也如同步代码那般直观,ctx的传递再也不需要构造专门的对象,我们可以直接通过lambda捕获参数传入。

Netplus里,几乎所有的IO调用,都是返回Promise对象,函数签名如下:

netp::ref_ptr<netp::promise<int>> ch_write(netp::ref_ptr<netp::packet> const& outlet);
netp::ref_ptr<netp::promise<int>> ch_write_to(netp::ref_ptr<netp::packet> const& outlet, address const& to);
netp::ref_ptr<netp::promise<int>> ch_close();
netp::ref_ptr<netp::channel_dial_promise<int>> dial(std::string const& host,...);

更多关于Promise,请参:

Concept: Promisegithub.com


3. 隐藏平台差异,将IO编程的共性进行抽象封装

将平台相关、IO事件处理、线程相关、性能相关,这些各APP里面都需要考虑的共性,进行抽象、封装。隐藏平台的细节和差异。

目前Netplus支持如下平台:

  • Windows on x86

  • Linux on x86/arm

  • IOS/MAC on x86/arm

  • Android on x86/arm

开发者再也不用关心平台以及平台相关的差异性。

4. 借鉴Netty,Pipeline化消息处理

Netplus在设计的时候,借鉴了很多来自netty的概念,如,channel, channel handler, pipeline, executor, scheduler, 那些熟悉 netty的朋友,就算不熟悉c++,应该也能快速上手。

通过Pipeline管理好Handler以及它的次序,便能如流水线一般对协议进行逐层处理,最终将业务需要的消息形态递送到业务逻辑的代码处。

通过此设计,使得开发者只需专注于自己的业务本身,或通过添加新的Handler,去适配自有协议,便能进行网络通迅。

为了便于快速开发,Netplus对http/https/websocket等协议提供了直接的支持,当然,也欢迎各位朋友为其添加其它的协议,让Netplus日渐丰满,我们的目标始终是,开箱即用,统统一把梭

03e1f3bf2f23e56088eeb9d218cdaf03.webp


欲穷千里目,更上一层楼,下文,我们将聊聊Netplus里面的一些基本概念。


浏览 22
点赞
评论
收藏
分享

手机扫一扫分享

分享
举报
评论
图片
表情
推荐
点赞
评论
收藏
分享

手机扫一扫分享

分享
举报