FFRPC应用 Client/Server使用及原理解析

摘要:

Ffrpc 进行了重构,精简了代码,代码更加清晰简洁,几乎完美的达到了我的预想。接下来将写几遍文章来介绍ffrpc可以做什么。简单总结ffrpc的特性是:

  • Ffrpc是c++ 网络通信库
  • 全异步 + 回调函数 机制
  • 支持普通二进制协议、protobuf、thrift
  • 基于Broker模式设计
  • 设计精巧,代码量小,核心ffrpc的代码只有1000行
  • 接口的性能监控是集成式的,使用者自动获得了接口性能数据,方便优化接口

普通二进制协议示例

Ffrpc实现了一个最基本的二进制序列化方法,基本的原理就是如果是固定长度那么就直接拷贝,如果是字符串,就先拷贝长度再拷贝内容。所以只支持向后扩展字段,对其他语言支持也不方便,但如果只是c++语言间传递消息,则显得非常的方便和高效。比如网游服务器中各个进程的通信可以采用这种最简单的二进制协议。Ffrpc中定义了一个工具类ffmsg_t来定义二进制消息.

消息定义:

struct echo_t
{
  struct in_t: public ffmsg_t<in_t>
  {
    void encode()
    {
      encoder() << data;
    }
    void decode()
    {
      decoder() >> data;
    }
    string data;
  };
  struct out_t: public ffmsg_t<out_t>
  {
    void encode()
    {
      encoder() << data;
    }
    void decode()
    {
      decoder() >> data;
    }
    string data;
  };
};

读者可以看到,ffmsg_t中提供了流式的序列化方法,使得序列化变得很容易。设计服务器消息的时候,需要注意的点有:

  • 在设计服务器接口的时候,每个接口接受一个消息作为参数,一个处理完毕返回一个消息,这是最传统的rpc模式。Ffrpc中采用这样的设计理念以简化和规范化接口设计。如果使用ffmsg_t定义消息,本人推荐的定义风格类似上面的代码这样。上面定义的是echo接口的输入消息和输出消息,但是都定义在echo_t结构内可以清晰的表明这是一对接口消息。
  • 传统的服务器接口会为每个接口定义一个cmd,然后通过cmd反序列化成特定的消息调用特定的接口,ffrpc省略了cmd的定义,而是直接采用消息名称作为cmd,比如在ffrpc中注册的接口接受echo_t的消息,那么收到echo_t的消息自然而言的是调用这个接口
  • 接口定义的时候必须的同时制定输入消息和输出消息
  • Ffmsg_t支持普通类型,字符串类型、stl类型。

echo服务的实现代码:

struct echo_service_t
{
  //! echo接口,返回请求的发送的消息ffreq_t可以提供两个模板参数,第一个表示输入的消息(请求者发送的)
  //! 第二个模板参数表示该接口要返回的结果消息类型
  void echo(ffreq_t<echo_t::in_t, echo_t::out_t>& req_)
  {
    echo_t::out_t out;
    out.data = req_.msg.data;
    LOGINFO(("XX", "foo_t::echo: recv %s", req_.msg.data.c_str()));

    req_.response(out);
  }
};
echo_service_t foo;
  //! broker客户端,可以注册到broker,并注册服务以及接口,也可以远程调用其他节点的接口
  ffrpc_t ffrpc_service("echo");
  ffrpc_service.reg(&echo_service_t::echo, &foo);

  if (ffrpc_service.open(arg_helper))
  {
    return -1;
}

这样就定义了echo服务,echo服务提供了一个接口,接受echo_t::in_t消息,返回echo_t::out_t消息。由此可见使用ffrpc定义服务的步骤是:

l 定义消息和接口

将接口注册到ffrpc的示例中,ffpc提供了reg模板方法,会自动的分析注册的接口使用神马输入消息,从而保证如果echo_t::in_t消息到来一定会调用对应的接口

Ffrpc工作的核心是broker,简单描述broker的作用就是转发消息。Ffrpc的client和server是不直接连接的,而是通过broker转发消息进行通信。

这样的好处是server的位置对于client是完全透明的,这也是broker模式最精髓的思想。所以ffrpc天生就是scalability的。Ffrpc的client比如要调用echo服务的接口,完全不需要知道serverr对应的位置或者配置,只需要知道echo服务的名字。有人可能担忧完全的broker转发可能会带来很大开销。

Broker保证了消息转发的最佳优化,如果client或者server和broker在同一进程,那么消息直接是内存间传递的,连序列化都不需要做,这也是得益于broker模式,broker模式的特点就是拥有很好的scalability。这样无论是简单的设计一个单进程的server还是设计成多进程分布式的一组服务,ffrpc都能完美胜任。
调用echo服务的client示例:

struct echo_client_t
{
  //! 远程调用接口,可以指定回调函数(也可以留空),同样使用ffreq_t指定输入消息类型,并且可以使用lambda绑定参数
  void echo_callback(ffreq_t<echo_t::out_t>& req_, int index, ffrpc_t* ffrpc_client)
  {
    if (req_.error())
    {
      LOGERROR(("XX", "error_msg <%s>", req_.error_msg()));
      return;
    }
    else if (index < 10)
    {
      echo_t::in_t in;
      in.data = "helloworld";
      LOGINFO(("XX", "%s %s index=%d callback...", __FUNCTION__, req_.msg.data.c_str(), index));
      sleep(1);
      ffrpc_client->call("echo", in, ffrpc_ops_t::gen_callback(&echo_client_t::echo_callback, this, ++index, ffrpc_client));
    }
    else
    {
      LOGINFO(("XX", "%s %s %d callback end", __FUNCTION__, req_.msg.data.c_str(), index));
    }
  }
};
ffrpc_t ffrpc_client;
  if (ffrpc_client.open(arg_helper))
  {
    return -1;
  }

  echo_t::in_t in;
  in.data = "helloworld";
  echo_client_t client;
  ffrpc_client.call("echo", in, ffrpc_ops_t::gen_callback(&echo_client_t::echo_callback, &client, 1, &ffrpc_client));

使用ffrpc调用远程接口,只需要制定服务名和输入消息,broker自动定位echo服务的位置,本示例中由于ffrpc的client和server在同一进程,那么自动通过内存间传递,如果server和broker在同一进程,而client在其他进程或者物理机上,则broker和server之间的传递为内存传递,broker和client的消息传递为tcp传输,这就跟自己写一个tcp的server收到消息投递给service的接口,然后将消息再通过tcp投递给client。但是必须看到,ffrpc完全简化了tcp server定义,并且更加scalability,甚至完全可以用来进程内多线程的通讯。

需要注意的是,ffrpc拥有良好的容错能力,如果服务不存在或者接口不存在或者异常等发生回调函数仍然是会被调用,并且返回错误信息,从而使错误处理变得更加容易。比如游戏服务器中client登入gate但是scene可能还没有启动的时候,这里就能够很好的处理,回调函数检查错误就可以了。对于回调函数,对于经常使用多线程和任务队列的开发者一定非常熟悉,回调函数支持lambda参数应该算是锦上添花,使得异步的代码变得更加清晰易懂。

Broker的启动方式:

int main(int argc, char* argv[])
  {
    //! 美丽的日志组件,shell输出是彩色滴!!
    LOG.start("-log_path ./log -log_filename log -log_class XX,BROKER,FFRPC -log_print_screen true -log_print_file false -log_level 3");
  
    if (argc == 1)
    {
      printf("usage: %s -broker tcp://127.0.0.1:10241\n", argv[0]);
      return 1;
    }
    arg_helper_t arg_helper(argc, argv);
  
    //! 启动broker,负责网络相关的操作,如消息转发,节点注册,重连等
  
    ffbroker_t ffbroker;
    if (ffbroker.open(arg_helper))
    {
      return -1;
    }
  
    sleep(1);
    if (arg_helper.is_enable_option("-echo_test"))
    {
      run_echo_test(arg_helper);
    }
    else if (arg_helper.is_enable_option("-protobuf_test"))
    {
      run_protobuf_test(arg_helper);
    }
    else
    {
      printf("usage %s -broker tcp://127.0.0.1:10241 -echo_test\n", argv[0]);
      return -1;
    }
  
    ffbroker.close();
    return 0;
  }

Ffrpc中两个关键的组件broker和rpc,broker负责转发和注册服务器,rpc则代表通信节点,可能是client可能是server。即使是多个服务器,只需要broker一个监听的端口,其他的服务只需要提供不同的服务名即可。

Protobuf协议示例

Ffrpc 良好的设计抽离了对于协议的耦合,使得支持protobuf就增加了10来行代码。当然这也是由于protobuf生成的消息都继承message基类。当我实现thrift的时候,事情就稍微麻烦一些,thrift生成的代码更加简洁,但是生成的消息不集成基类,需要复制粘贴一些代码。

Protobuf的定义文件:

package ff;

message pb_echo_in_t {
 required string data = 1;
}
message pb_echo_out_t {
 required string data = 1;
}

我们仍然设计一个echo服务,定义echo接口的消息,基于ffrpc的设计理念,每个接口都有一个输入消息和输出消息。

Echo服务的实现代码:

struct protobuf_service_t
{
  //! echo接口,返回请求的发送的消息ffreq_t可以提供两个模板参数,第一个表示输入的消息(请求者发送的)
  //! 第二个模板参数表示该接口要返回的结果消息类型
  void echo(ffreq_t<pb_echo_in_t, pb_echo_out_t>& req_)
  {
    LOGINFO(("XX", "foo_t::echo: recv data=%s", req_.msg.data()));
    pb_echo_out_t out;
    out.set_data("123456");
    req_.response(out);
  }
};
protobuf_service_t foo;
  //! broker客户端,可以注册到broker,并注册服务以及接口,也可以远程调用其他节点的接口
  ffrpc_t ffrpc_service("echo");
  ffrpc_service.reg(&protobuf_service_t::echo, &foo);

  if (ffrpc_service.open(arg_helper))
  {
    return -1;
  }

跟使用ffmsg_t的方式几乎是一样的,ffreq_t 的msg字段是输入的消息。

调用echo服务器的client的示例代码

struct protobuf_client_t
{
  //! 远程调用接口,可以指定回调函数(也可以留空),同样使用ffreq_t指定输入消息类型,并且可以使用lambda绑定参数
  void echo_callback(ffreq_t<pb_echo_out_t>& req_, int index, ffrpc_t* ffrpc_client)
  {
    if (req_.error())
    {
      LOGERROR(("XX", "error_msg <%s>", req_.error_msg()));
      return;
    }
    else if (index < 10)
    {
      pb_echo_in_t in;
      in.set_data("Ohnice");
      LOGINFO(("XX", "%s data=%s index=%d callback...", __FUNCTION__, req_.msg.data(), index));
      sleep(1);
      ffrpc_client->call("echo", in, ffrpc_ops_t::gen_callback(&protobuf_client_t::echo_callback, this, ++index, ffrpc_client));
    }
    else
    {
      LOGINFO(("XX", "%s %d callback end", __FUNCTION__, index));
    }
  }
};

  ffrpc_t ffrpc_client;
  if (ffrpc_client.open(arg_helper))
  {
    return -1;
  }

  protobuf_client_t client;
  pb_echo_in_t in;
  in.set_data("Ohnice");

  ffrpc_client.call("echo", in, ffrpc_ops_t::gen_callback(&protobuf_client_t::echo_callback, &client, 1, &ffrpc_client));

Protobuf的优点是:

支持版本,这样增加字段变得更加容易

Protobuf是支持多语言的,这样可以跟其他的语言也可以通讯

Thrift协议的示例

Thrift 定义文件:

namespace cpp ff 

struct echo_thrift_in_t {
 1: string data
}

struct echo_thrift_out_t {
 1: string data
}

Thrift 的服务器实现代码:

struct thrift_service_t
{
  //! echo接口,返回请求的发送的消息ffreq_t可以提供两个模板参数,第一个表示输入的消息(请求者发送的)
  //! 第二个模板参数表示该接口要返回的结果消息类型
  void echo(ffreq_thrift_t<echo_thrift_in_t, echo_thrift_out_t>& req_)
  {
    LOGINFO(("XX", "foo_t::echo: recv data=%s", req_.msg.data));
    echo_thrift_out_t out;
    out.data = "123456";
    req_.response(out);
  }
};
thrift_service_t foo;
  //! broker客户端,可以注册到broker,并注册服务以及接口,也可以远程调用其他节点的接口
  ffrpc_t ffrpc_service("echo");
  ffrpc_service.reg(&thrift_service_t::echo, &foo);

  if (ffrpc_service.open(arg_helper))
  {
    return -1;
  }

  ffrpc_t ffrpc_client;
  if (ffrpc_client.open(arg_helper))
  {
    return -1;
}

调用 echo的client的示例:

struct thrift_client_t
{
  //! 远程调用接口,可以指定回调函数(也可以留空),同样使用ffreq_t指定输入消息类型,并且可以使用lambda绑定参数
  void echo_callback(ffreq_thrift_t<echo_thrift_out_t>& req_, int index, ffrpc_t* ffrpc_client)
  {
    if (req_.error())
    {
      LOGERROR(("XX", "error_msg <%s>", req_.error_msg()));
      return;
    }
    else if (index < 10)
    {
      echo_thrift_in_t in;
      in.data = "Ohnice";
      LOGINFO(("XX", "%s data=%s index=%d callback...", __FUNCTION__, req_.msg.data, index));
      sleep(1);
      ffrpc_client->call("echo", in, ffrpc_ops_t::gen_callback(&thrift_client_t::echo_callback, this, ++index, ffrpc_client));
    }
    else
    {
      LOGINFO(("XX", "%s %d callback end", __FUNCTION__, index));
    }
  }
};
ffrpc_t ffrpc_client;
  if (ffrpc_client.open(arg_helper))
  {
    return -1;
  }

  thrift_client_t client;
  echo_thrift_in_t in;
  in.data = "Ohnice";

ffrpc_client.call("echo", in, ffrpc_ops_t::gen_callback(&thrift_client_t::echo_callback, &client, 1, &ffrpc_client));

Thrift的优缺点:

  • Thrift 更加灵活,支持list和map,而且可以嵌套
  • 支持N种语言
  • 官方的版本需要依赖boost,ffrpc从中提取出一个最基本的c++版本,只有头文件,不依赖boost

总结

  • Ffrpc是基于c++的网络通讯库,基于broker模式scalability和 易用性是最大的优点
  • 使用ffrpc进行进程间通讯非常的容易,定义服务和接口就行了,你除了使用ffmsg_t最传统的消息定义,也可以使用google protobuf和facebook thrift。
  • Ffrpc是全异步的,通过回调函数+lambda方式可以很容易操作异步逻辑。
  • Ffrpc 接下来会有更多的示例,当系统复杂时,ffrpc的优势将会更加明显。
  • Github的地址: https://github.com/fanchy/FFRPC

以上就是本文的全部内容,希望对大家的学习有所帮助,也希望大家多多支持我们。

(0)

相关推荐

  • SpringBoot集成gRPC微服务工程搭建实践的方法

    前言 本文将使用Maven.gRPC.Protocol buffers.Docker.Envoy等工具构建一个简单微服务工程,笔者所使用的示例工程是以前写的一个Java后端工程,因为最近都在 学习微服务相关的知识,所以利用起来慢慢的把这个工程做成微服务化应用.在实践过程踩过很多坑,主要是经验不足对微服务还是停留在萌新阶段,通过本文 记录创建微服务工程碰到一些问题,此次实践主要是解决以下问题: 如何解决.统一服务工程依赖管理 SpringBoot集成gRPC 管理Protocol buffers文

  • 对python调用RPC接口的实例详解

    要调用RPC接口,python提供了一个框架grpc,这是google开源的 rpc相关文档: https://grpc.io/docs/tutorials/basic/python.html 需要安装的python包如下: 1.grpc安装 pip install grpcio 2.grpc的python protobuf相关的编译工具 pip install grpcio-tools 3.protobuf相关python依赖库 pip install protobuf 4.一些常见原型的生成

  • python使用rpc框架gRPC的方法

    概述 gRPC 是谷歌开源的一个rpc(远程程序调用)框架,可以轻松实现跨语言,跨平台编程,其采用gRPC协议(基于HTTP2). rpc: remote procedure call, 翻译过来就是是远程程序调用.具体来说,就是客户端c1需要调用服务器s1上的某个方法(函数),得到相应的返回值并传递给c1. gRPC协议 要说gRPC协议需要先了解HTTP2, 虽然HTTP1.X 协议至今仍是主流协议,但是随着我们对性能要求越来越高,和web规模的不断扩大,HTTP2就应运而生. 在这里,我们

  • SpringBoot2.0 整合 Dubbo框架实现RPC服务远程调用方法

    一.Dubbo框架简介 1.框架依赖 图例说明: 1)图中小方块 Protocol, Cluster, Proxy, Service, Container, Registry, Monitor 代表层或模块,蓝色的表示与业务有交互,绿色的表示只对 Dubbo 内部交互. 2)图中背景方块 Consumer, Provider, Registry, Monitor 代表部署逻辑拓扑节点. 3)图中蓝色虚线为初始化时调用,红色虚线为运行时异步调用,红色实线为运行时同步调用. 4)图中只包含 RPC

  • 在Python中使用gRPC的方法示例

    本文介绍了在Python中使用gRPC的方法示例,分享给大家,具体如下: 使用Protocol Buffers的跨平台RPC系统. 安装 使用 pip pip install grpcio pip install grpcio-tools googleapis-common-protos gRPC由两个部分构成,grpcio 和 gRPC 工具, 后者是编译 protocol buffer 以及提供生成代码的插件. 使用 编写protocol buffer 使用 gRPC 首先需要做的是设计 p

  • Nginx配置代理gRPC的方法

    Nginx 1.13.10新增了对gRPC的原生支持.本文介绍如何配置Nginx的gRPC. 安装Nginx Nginx版本要求:1.13.10. gRPC必须使用HTTP/2传输数据,支持明文和TLS加密数据,支持流数据的交互.这是为了充分利用 HTTP/2 连接的多路复用和流式特性.所以在安装部署nginx时需要安装http/2.使用源码安装,编译时需要加入http_ssl和http_v2模块: $ auto/configure --with-http_ssl_module --with-h

  • 图析ASP.NET Core引入gRPC服务模板

    早就听说ASP.NET Core 3.0中引入了gRPC的服务模板,正好趁着家里电脑刚做了新系统,然后装了VS2019的功夫来体验一把.同时记录体验的过程.如果你也想按照本文的步骤体验的话,那你得先安装.NET Core3.0预览版的SDK.至于开发工具我用的时VS2019,当然你也可以使用VS Code进行. gRPC的简单介绍 gRPC 是一种与语言无关的高性能远程过程调用 (RPC) 框架. 有关 gRPC 基础知识的详细信息,请参阅 gRPC 文档页. gRPC 的主要优点是: 现代高性

  • FFRPC应用 Client/Server使用及原理解析

    摘要: Ffrpc 进行了重构,精简了代码,代码更加清晰简洁,几乎完美的达到了我的预想.接下来将写几遍文章来介绍ffrpc可以做什么.简单总结ffrpc的特性是: Ffrpc是c++ 网络通信库 全异步 + 回调函数 机制 支持普通二进制协议.protobuf.thrift 基于Broker模式设计 设计精巧,代码量小,核心ffrpc的代码只有1000行 接口的性能监控是集成式的,使用者自动获得了接口性能数据,方便优化接口 普通二进制协议示例 Ffrpc实现了一个最基本的二进制序列化方法,基本的

  • spring boot jar的启动原理解析

     1.前言 近来有空对公司的open api平台进行了些优化,然后在打出jar包的时候,突然想到以前都是对spring boot使用很熟练,但是从来都不知道spring boot打出的jar的启动原理,然后这回将jar解开了看了下,与想象中确实大不一样,以下就是对解压出来的jar的完整分析. 2.jar的结构 spring boot的应用程序就不贴出来了,一个较简单的demo打出的结构都是类似,另外我采用的spring boot的版本为1.4.1.RELEASE网上有另外一篇文章对spring

  • spring cloud Ribbon用法及原理解析

    这篇文章主要介绍了spring cloud Ribbon用法及原理解析,文中通过示例代码介绍的非常详细,对大家的学习或者工作具有一定的参考学习价值,需要的朋友可以参考下 简介 这篇文章主要介绍一下ribbon在程序中的基本使用,在这里是单独拿出来写用例测试的,实际生产一般是配置feign一起使用,更加方便开发.同时这里也通过源码来简单分析一下ribbon的基本实现原理. 基本使用 这里使用基于zookeeper注册中心+ribbon的方式实现一个简单的客户端负载均衡案例. 服务提供方 首先是一个

  • RabbitMQ简单队列实例及原理解析

    这篇文章主要介绍了RabbitMQ简单队列实例及原理解析,文中通过示例代码介绍的非常详细,对大家的学习或者工作具有一定的参考学习价值,需要的朋友可以参考下 RabbitMQ 简述 RabbitMQ是一个消息代理:它接受并转发消息. 您可以将其视为邮局:当您将要把寄发的邮件投递到邮箱中时,您可以确信Postman 先生最终会将邮件发送给收件人. 在这个比喻中,RabbitMQ是一个邮箱,邮局和邮递员,用来接受,存储和转发二进制数据块的消息. 队列就像是在RabbitMQ中扮演邮箱的角色. 虽然消息

  • 从云数据迁移服务看MySQL大表抽取模式的原理解析

    摘要:MySQL JDBC抽取到底应该采用什么样的方式,且听小编给你娓娓道来. 小编最近在云上的一个迁移项目中被MySQL抽取模式折磨的很惨.一开始爆内存被客户怼,再后来迁移效率低下再被怼.MySQL JDBC抽取到底应该采用什么样的方式,且听小编给你娓娓道来. 1.1 Java-JDBC通信原理 JDBC与数据库之间的通信是通过socket完,大致流程如下图所示.Mysql Server ->内核Socket Buffer -> 客户端Socket Buffer ->JDBC所在的JV

  • React服务端渲染原理解析与实践

    关于服务端渲染也就是我们说的SSR大多数人都听过这个概念,很多同学或许在公司中已经做过服务端渲染的项目了,主流的单页面应用比如说Vue或者React开发的项目采用的一般都是客户端渲染的模式也就是我们说的CSR. 但是这种模式会带来明显的两个问题,第一个就是TTFP时间比较长,TTFP指的就是首屏展示时间,同时不具备SEO排名的条件,搜索引擎上排名不是很好.所以我们可以借助一些工具来进行改良我们的项目,将单页面应用编程服务器端渲染项目,这样就可以解决掉这些问题了. 目前主流的服务器端渲染框架也就是

  • asp防止上传图片木马原理解析

    首先判断文件大小: if file.filesize<10 then Response.Write("<script>alert('您没有选择上传文件')</script>") Response.Write("<script>history.go(-1)</script>") Response.End() end if 将文件上传到服务器后,判断用户文件中的危险操作字符: set MyFile = server.

  • SpringCloud配置刷新原理解析

    我们知道在SpringCloud中,当配置变更时,我们通过访问http://xxxx/refresh,可以在不启动服务的情况下获取最新的配置,那么它是如何做到的呢,当我们更改数据库配置并刷新后,如何能获取最新的数据源对象呢?下面我们看SpringCloud如何做到的. 一.环境变化 1.1.关于ContextRefresher 当我们访问/refresh时,会被RefreshEndpoint类所处理.我们来看源代码: /* * Copyright 2013-2014 the original a

  • Python 中 -m 的典型用法、原理解析与发展演变

    在命令行中使用 Python 时,它可以接收大约 20 个选项(option),语法格式如下: python [-bBdEhiIOqsSuvVWx?] [-c command | -m module-name | script | - ] [args] 本文想要聊聊比较特殊的"-m"选项: 关于它的典型用法.原理解析与发展演变的过程. 首先,让我们用"--help"来看看它的解释: -m mod run library module as a script (ter

  • python实现布隆过滤器及原理解析

    在学习redis过程中提到一个缓存击穿的问题, 书中参考的解决方案之一是使用布隆过滤器, 那么就有必要来了解一下什么是布隆过滤器.在参考了许多博客之后, 写个总结记录一下. 一.布隆过滤器简介 什么是布隆过滤器? 本质上布隆过滤器( BloomFilter )是一种数据结构,比较巧妙的概率型数据结构(probabilistic data structure),特点是高效地插入和查询,可以用来告诉你 "某样东西一定不存在或者可能存在". 相比于传统的 Set.Map 等数据结构,它更高效

随机推荐