实现一个迷你版的RPC

前言

在实际后台服务开发中,比如订单服务(开发者A负责)需要调用商品服务(开发者B负责),那么开发者B会和A约定调用API,以接口的形式提供给A。通常都是B把API上传到Maven私服,然后B开始写API的实现,A只需要引入API依赖进行开发即可。

订单服务调用商品服务

上图简单的描述了RPC在实际场景中的应用,我们在开发中当然是利用现有的RPC框架来快速实现业务需求,比如百度开源了baidu-rpc,阿里的Dubbo早已声名在外,腾讯自己玩TAF。本篇博客将实现一个迷你版的RPC,探索下RPC底层实现的奥秘!

 

动手实现RPC

商品服务工程

商品服务工程

注意,我将商品服务的API以及实现分为Maven的2个模块来开发。这里,我们想给定一个商品ID,查询得到商品对象信息。

商品对象

商品API

Product

要注意的是,Product是可以被序列化的,Why?

很显然,订单系统调用商品系统的时候,需要商品系统返回一个商品,必然涉及到发生网络传输,这就涉及对象的序列化和反序列化了。

商品查询API接口

商品查询API

订单系统调用商品服务

订单服务调用商品服务

在订单系统工程中需要引入商品服务API依赖。

在上图代码中,最重要的就是rpc方法了!

rpc实现方法

rpc

第一,我们看到了Proxy.newProxyInstance,很显然在进行动态代理。也即是说,在订单服务调用商品服务的代码中,我们先是通过动态代理返回一个代理的IProductService类型对象,这意味着当代理对象调用queryById方法的时候,会自动调用invoke方法!

第二,我们看看invoke到底做了些什么?

它本质上就是进行Socket通信,那么它需要传递什么信息给到商品服务呢?

我们知道订单系统就是想调用商品服务的某个类的某个方法,然后把这个方法的返回结果传输给订单系统!

想一想,如何调用某个类的某个方法呢?

只要我们能确定这个类的全限定类名、确定方法名、确定方法的参数类型,给定方法需要的具体参数,通过反射就能实现。

商品服务调用后得到的结果,我们序列化写入Socket流中,在订单系统中反序列化得到对象即可。

第三,这里需要思考一个问题:在订单系统中我们只知道商品服务的API,并不知道这背后的API到底是如何实现的,所以我们需要有一个映射,就是商品服务的API到商品服务的实现的一个映射关系,其实这就是所谓的服务的注册!

商品API的具体实现

商品服务

 

商品服务API实现

商品服务

商品服务

从这里,可以清晰的看到,商品服务读取了订单系统调用商品系统时发送的数据,利用反射机制,进行方法调用,并把调用结果写入Socket输出流。

运行结果

运行结果

启动商品服务后,通过订单系统发起对商品服务的调用。

 

以前总认为RPC是遥不可及的,感觉是个很神奇的东西,实际上它的底层实现不就是这样的么~

 

就这样我们实现了一个简陋而又详细的RPC。
说它简陋,是因为这个实现确实比较挫,在下一小节会说它为什么挫。
说它详细,是因为它一步一步的演示了一个RPC的执行流程,方便大家了解RPC的内部机制。

为什么说这个RPC实现很挫???

这个RPC实现只是为了给大家演示一下RPC的原理,要是想放到生产环境去用,那是绝对不行的。

1、缺乏通用性
我通过给Calculator接口写了一个CalculatorRemoteImpl,来实现计算器的远程调用,下一次要是有别的接口需要远程调用,是不是又得再写对应的远程调用实现类?这肯定是很不方便的。

那该如何解决呢?先来看看使用Dubbo时是如何实现RPC调用的:

@Reference
private Calculator calculator;

...

calculator.add(1,2);

...

Dubbo通过和Spring的集成,在Spring容器初始化的时候,如果扫描到对象加了@Reference注解,那么就给这个对象生成一个代理对象,这个代理对象会负责远程通讯,然后将代理对象放进容器中。所以代码运行期用到的calculator就是那个代理对象了。

我们可以先不和Spring集成,也就是先不采用依赖注入,但是我们要做到像Dubbo一样,无需自己手动写代理对象,怎么做呢?那自然是要求所有的远程调用都遵循一套模板,把远程调用的信息放到一个RpcRequest对象里面,发给Server端,Server端解析之后就知道你要调用的是哪个RPC接口、以及入参是什么类型、入参的值又是什么,就像Dubbo的RpcInvocation:

public class RpcInvocation implements Invocation, Serializable {

    private static final long serialVersionUID = -4355285085441097045L;

    private String methodName;

    private Class<?>[] parameterTypes;

    private Object[] arguments;

    private Map<String, String> attachments;

    private transient Invoker<?> invoker;

2、集成Spring
在实现了代理对象通用化之后,下一步就可以考虑集成Spring的IOC功能了,通过Spring来创建代理对象,这一点就需要对Spring的bean初始化有一定掌握了。

3、长连接or短连接
总不能每次要调用RPC接口时都去开启一个Socket建立连接吧?是不是可以保持若干个长连接,然后每次有rpc请求时,把请求放到任务队列中,然后由线程池去消费执行?只是一个思路,后续可以参考一下Dubbo是如何实现的。

4、 服务端线程池
我们现在的Server端,是单线程的,每次都要等一个请求处理完,才能去accept另一个socket的连接,这样性能肯定很差,是不是可以通过一个线程池,来实现同时处理多个RPC请求?同样只是一个思路。

5、服务注册中心
正如之前提到的,要调用服务,首先你需要一个服务注册中心,告诉你对方服务都有哪些实例。Dubbo的服务注册中心是可以配置的,官方推荐使用Zookeeper。如果使用Zookeeper的话,要怎样往上面注册实例,又要怎样获取实例,这些都是要实现的。

6、负载均衡
如何从多个实例里挑选一个出来,进行调用,这就要用到负载均衡了。负载均衡的策略肯定不只一种,要怎样把策略做成可配置的?又要如何实现这些策略?同样可以参考Dubbo

7、结果缓存
每次调用查询接口时都要真的去Server端查询吗?是不是要考虑一下支持缓存?

8、多版本控制
服务端接口修改了,旧的接口怎么办?

9、异步调用
客户端调用完接口之后,不想等待服务端返回,想去干点别的事,可以支持不?

10、优雅停机
服务端要停机了,还没处理完的请求,怎么办?

......

诸如此类的优化点还有很多,这也是为什么实现一个高性能高可用的RPC框架那么难的原因。



 

RPC(Remote Procedure Call)是一种远程调用协议,它允许客户端程序调用远程服务器上的函数或方法。C++可以使用一些库来实现RPC服务,其中比较流行的有 gRPC 和 Apache Thrift。 以下是一个使用 gRPC 的简易 RPC 服务的示例: 1. 首先,需要安装 gRPC 和 Protocol Buffers: ``` sudo apt install -y build-essential autoconf libtool pkg-config grpc libgrpc++-dev protobuf-compiler-grpc ``` 2. 创建一个 Protocol Buffers 文件 `example.proto`,定义 RPC 服务的接口: ``` syntax = "proto3"; package example; service ExampleService { rpc SayHello (HelloRequest) returns (HelloResponse) {} } message HelloRequest { string name = 1; } message HelloResponse { string message = 1; } ``` 3. 使用 Protocol Buffers 编译器生成 C++ 代码: ``` protoc --grpc_out=. --cpp_out=. example.proto ``` 4. 实现 RPC 服务的接口: ``` #include <iostream> #include <memory> #include <string> #include <grpcpp/grpcpp.h> #include "example.grpc.pb.h" using grpc::Server; using grpc::ServerBuilder; using grpc::ServerContext; using grpc::Status; using example::HelloRequest; using example::HelloResponse; using example::ExampleService; class ExampleServiceImpl final : public ExampleService::Service { Status SayHello(ServerContext* context, const HelloRequest* request, HelloResponse* response) override { std::string prefix("Hello "); response->set_message(prefix + request->name()); return Status::OK; } }; void RunServer() { std::string server_address("0.0.0.0:50051"); ExampleServiceImpl service; grpc::ServerBuilder builder; builder.AddListeningPort(server_address, grpc::InsecureServerCredentials()); builder.RegisterService(&service); std::unique_ptr<Server> server(builder.BuildAndStart()); std::cout << "Server listening on " << server_address << std::endl; server->Wait(); } int main(int argc, char** argv) { RunServer(); return 0; } ``` 5. 编译并运行服务器代码: ``` g++ -std=c++11 -I. -I/usr/local/include -L/usr/local/lib example.pb.cc example.grpc.pb.cc example_server.cc -lgrpc++ -lgrpc -lgpr -lprotobuf -lpthread -o example_server ./example_server ``` 6. 编客户端代码: ``` #include <iostream> #include <memory> #include <string> #include <grpcpp/grpcpp.h> #include "example.grpc.pb.h" using grpc::Channel; using grpc::ClientContext; using grpc::Status; using example::HelloRequest; using example::HelloResponse; using example::ExampleService; class ExampleClient { public: ExampleClient(std::shared_ptr<Channel> channel) : stub_(ExampleService::NewStub(channel)) {} std::string SayHello(const std::string& name) { HelloRequest request; request.set_name(name); HelloResponse response; ClientContext context; Status status = stub_->SayHello(&context, request, &response); if (status.ok()) { return response.message(); } else { return "RPC failed"; } } private: std::unique_ptr<ExampleService::Stub> stub_; }; int main(int argc, char** argv) { ExampleClient client(grpc::CreateChannel("localhost:50051", grpc::InsecureChannelCredentials())); std::string name("World"); std::string reply = client.SayHello(name); std::cout << "Received: " << reply << std::endl; return 0; } ``` 7. 编译并运行客户端代码: ``` g++ -std=c++11 -I. -I/usr/local/include -L/usr/local/lib example.pb.cc example.grpc.pb.cc example_client.cc -lgrpc++ -lgrpc -lgpr -lprotobuf -lpthread -o example_client ./example_client ``` 以上是一个简易的使用 gRPC 实现RPC 服务和客户端的示例。
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值