欢迎光临
我们一直在努力

工作中 C++ 泛型编程用的多吗?



1412的回答

这个问题请务必让我来一发!去年一个项目里用了8、9种方式,绝对是实战上值得拿出来和大家分享的一段经历!其实泛型并不是越多越好,我认为泛型会增加代码本身的复杂性(哪怕它能减少代码量),只是当我们面临具体问题,发现需要一些解耦的方案时,知道能有什么方法可以用就行了。

这个项目里,一套代码支持了:

  1. 不同网络通讯;
  2. 不同应用层协议;
  3. 不同IDL;
  4. 不同数据结构;
  5. 不同压缩类型;
  6. 不同功能的插件;

纵向可以增加新的层次,横向也可以随便扩展新功能。

这都是用各种天花乱坠的泛型方式来实现的代码统一,有些实现方式系统来说也许不叫泛型编程,但目的都是为了动态可扩展地解耦合。

项目在这里↓↓↓ 然后我们开始看看为什么、和怎么用:

sogou/srpc

项目的wiki介绍过架构,今天终于有机会单独把泛型的做法拿出来写一下 ~~~>_<~~~

♦ 第一个问题:RPC框架希望支持两种最热门的IDL:protobuf和thrift,但大家的序列化函数不一样,怎么做到具体哪个IDL就调哪个函数呢?

【方法1:同名函数】

class RPCMessage
{
pubic:
 virtual int serialize(const ProtobufIDLMessage *idl_msg);
 virtual int serialize(const ThriftIDLMessage *idl_msg);
 
};

♦ 第二个问题:目前SPRC除了自己的sogou-std协议以外、还支持了tRPC和BRPC协议,其中tRPC协议是目前唯一一个开源实现。那么如何支持不同RPC应用层协议?

【方法2:模版】

我们知道,SRPC是基于Workflow做的RPC框架,一次异步网络交互就是一个任务,任务本身的行为是收发、序列化、压缩本身,而和具体什么协议是没有关系的,所以协议本身可以抽象为两个模版:REQRESP,则我们一个网络交互就是长这样的:

template<class RPCREQ, class RPCRESP>
class RPCClientTask : public WFComplexClientTask<RPCREQ, RPCRESP>
{
// 任务本身的收发和协议没关系,协议只需要告诉我怎么算收完就行
};

♦ 第三个问题:RPC协议里一般都会有一个框架相关的可扩展头部,我们称为meta,其可扩展是比如借用IDL自身的前后兼容能力实现的。那么不同meta的检查各协议不同,如何解决呢?

【方法3:虚函数】

class RPCRequest
{
public:
 virtual bool serialize_meta() = 0;
 virtual bool deserialize_meta() = 0;
...
}

然后各不同协议有不同的实现即可,虚函数非常常见和好使~

♦ 第四个问题:刚才说的不同压缩类型,如何根据compress type找到我要的压缩函数?

【方法4:函数指针】

这是最基本、最原始的泛型编程方式,很多上层的泛型其实也是借用函数指针来实现的。

这里分三步讲一下:全局留好函数指针、具体函数指针的实现、注册到全局。

1)我们支持gzip、zlib、snappy、lz4,这些压缩库有些是纯C代码,因此我们对每种类型都包成了一个CompressHandler

using CompressFunction = int (*)(const char*, size_t, char *, size_t);
using DecompressFunction = int (*)(const char *, size_t, char *, size_t);

class CompressHandler
{
public:
    CompressHandler()
    {   
        //this->type = RPCCompressNone;
        this->compress = nullptr;
        this->decompress = nullptr;
        ...
    }   

    //int type;
    CompressFunction compress;
    DecompressFunction decompress;
    ...
};

2) 给snappy实现它的压缩/解压函数,内部调用snappy的api:

class SnappyManager
{
public:
    static int SnappyCompress(const char *msg, size_t msglen, char *buf, size_t buflen);
    static int SnappyDecompress(const char *buf, size_t buflen, char *msg, size_t msglen);
    ...
};

3) 然后,注册上去:

 switch (type)
 { 
   case RPCCompressSnappy:
   this->handler[type].compress = SnappyManager::SnappyCompress;
   this->handler[type].decompress = SnappyManager::SnappyDecompress;
   ...

这样RPCMessage调用compress()的时候就可以根据压缩类型自动找到函数指针了~

♦ 第五个问题:如何支持跨协议呢?比如,对方发过来HTTP请求带上protobuf的内容(而不是二进制的SRPC协议),我照样可以用这套RPC框架做反序列化的事情。

【方法5:共同派生和交叉调用】

由于这里派生关系比较复杂,直接上图。

用过Workflow的小伙伴都知道,消息收发是每个消息各自实现的append()函数,所以Workflow中的HttpMessage::append()已经有现成的实现,可以帮我们收Http协议了。

只要我们对二进制协议按照二进制协议去实现append()把消息收完,然后单独的用户IDL数据部分大家都作为body拎出来,body在中间的SRPCHttpRequest/SRPCHttpResponse层做反序列化,则可以复用Workflow的消息解析Http的代码了(机智如我!

~~~~~~~~~~~~~~~~~~~~~~~~~

插播:所以到这里,我们就可以实现:同一份用户service实现,放到不同的协议server里混用了!!!这里以protobuf的service作为例子看一下:

class ExampleServiceImpl : public ::example::ExamplePB::Service
{
public:
    void Echo(::example::EchoRequest *request, ::example::EchoResponse *response,
              srpc::RPCContext *ctx) override
    { /* 收到请求后要做的事情,并填好回复;*/ }  
};


int main()
{
    SRPCServer server_srpc;
    SRPCHttpServer server_srpc_http;
    BRPCServer server_brpc;
    TRPCServer server_trpc;
    ThriftServer server_thrift;

    ExampleServiceImpl impl_pb;                   // 使用pb作为接口的service
    AnotherThriftServiceImpl impl_thrift;         // 使用thrift作为接口的service

    server_srpc.add_service(&impl_pb);            // 只要协议本身支持这种IDL,就可以把这类service往里加
    server_srpc.add_service(&impl_thrift); 
    server_srpc_http.add_service(&impl_pb);       // srpc还可以同时提供TCP和Http服务
    server_srpc_http.add_service(&impl_thrift);
    server_brpc.add_service(&impl_pb);            // baidu-std协议只支持了protobuf
    server_trpc.add_service(&impl_pb);            // 只需要改一个字母,就可以方便兼容不同协议
    server_thrift.add_service(&impl_thrift);      // thrift-binary协议只支持了thrift

    server_srpc.start(1412);
    server_srpc_http.start(8811);
    server_brpc.start(2020);
    server_trpc.start(2021);
    server_thrift.start(9090);
    ....

    return 0;
}

~~~~~~~~~~~~~~~~~~~~~~~~~

下面的几种方式并没有那么通用,所以简单列举一下,不细展开。尤其是6和7,本质上是解决IDL的问题:用户定义的结构如何序列化/反序列化和生成api的问题。

♦ 第六个问题:不想用protobuf生成的默认service接口,原始接口太繁琐,希望生成一套满足Workflow任务流的service接口。

【方法6:自己做代码生成】解决IDL中的service和method

♦ 第七个问题:thrift的网络与序列和分离不如protobuf好,但解析thrift IDL不想链接thrift库,而是希望Workflow做网络收发、SRPC做IDL解析。

【方法7:自己做内存反射】解决IDL中的message

♦ 第八个问题:SRPC是基于Workflow异步框架的二次开发,而Workflow是回调函数模式的,SRPC有许多事情需要在Workflow和用户之间做,比如反序列化。

【方法8:接管用户回调】回调函数std::function本身就是可以自带上下文~

♦ 第九个问题:用户做二次开的可扩展功能如何支持?

【方法9:模块化插件】

用一个图来讲解一下模块化插件吧,这个思路和nginx的filter是类似的:一个请求/回复,我们作为框架态可以为开发者提供接口,让开发者做一些独立的请求处理模块,以实现鉴权、监控等事情。

~~~~~~~~~~~~~~~~~~~~~~~~~

写完回答发现又摸鱼了俩小时(Q_Q) 最后梳理一下这些泛型方法所做出来的层次和功能:

  • 用户代码(client的发送函数/server的函数实现)
  • IDL序列化(protobuf/thrift serialization)
  • 数据组织 (protobuf/thrift/json)
  • 压缩(none/gzip/zlib/snappy/lz4)
  • 协议 (Sogou-std/Baidu-std/Thrift-framed/TRPC)
  • 通信 (TCP/HTTP)

SRPC整体代码约仅有1万行,以上内容都包括在同一套代码里,解耦的时候就像在做一个艺术品一样去打磨,希望能用最精巧和最灵活的方式来实现层级之间的调用,并且可以以最小的代价增加每个层级中的新功能~~~

各位小伙伴对于这些做法有什么建议都欢迎多多交流哟!!!(^∇^)

————————

由于突然很多小伙伴点赞,让我受宠若惊 ///>~</// 因此我在改某些错别字的同时,顺带补充一下,这里介绍的方法并不是都是理论上的“泛型编程”,有些算是实践上广义的多态,但要解决的问题是一样的,所以才更加觉得值得与大家分享~~~拿着实际项目代码来交流也觉得会比较有诚意~希望这篇回答能够给大家看到与实践的结合,我们其实有许多种途径去追寻架构中的结构之美。



Source link

本站文章是收集于互联网,如有版权问题,请联系博主及时删除! 如未注明,均为原创,版权所有,转载请注明原文链接.坏土豆 » 工作中 C++ 泛型编程用的多吗?

分享到: 生成海报

评论 抢沙发

*
  • QQ号
  • 昵称 (必填)
  • 邮箱 (必填)
  • 网址
切换注册

登录

忘记密码 ?

切换登录

注册

我们将发送一封验证邮件至你的邮箱, 请正确填写以完成账号注册和激活