本文分析的 RPC 框架只是一种可行的 RPC 框架。基于开源库:https://github.com/youngyangyang04/Krpc
本文不会进行系统介绍,只是分享个人思考。
总体架构介绍
总体架构图,下文还会用到
在客户看来,RPC 框架提供了 Rpc_stub 类。客户编写代码时,可以创建这个类的对象,调用其方法。客户不关系方法内部的实现,只需要知道方法是做什么的。客户不知道的是:方法内部会发起网络请求,要求服务端来执行相应功能并返回结果。
RPC 框架需要为客户提供上述网络通信的底层实现,这主要涉及到网络编程和数据(RPC方法参数和返回值等)序列化和反序列化两个问题。使用 protobuf 可以帮助 RPC 框架开发者解决数据序列化和反序列化的问题。
下文介绍 ProtoBuf、RPC 框架实现者、RPC 框架用户三者做的工作,注意分清楚每部分代码都来自于哪里,是生成的还是谁负责编写的,可以参考总体架构图中的分类。
ProtoBuf 的作用
使用 ProtoBuf,可以通过在 .proto 文件中描述数据结构,利用 proto 编译器自动生成一套代码(c++或其他支持的语言,对于 c++,会生成 .cc 和 .h 文件),这套代码包括你描述的数据结构的定义,还包括各种序列化和反序列化这些数据结构的方法。下面示例中所有 message 都是自定义的数据结构。
// .proto 文件示例
syntax="proto3";
package Kuser;
option cc_generic_services=true;
message ResultCode{
int32 errcode=1;
bytes errmsg=2;
}
message LoginRequest{
bytes name=1;
bytes pwd=2;
}
message LoginResponse{
ResultCode result=1;
bool success=2;
}
message RegisterRequest{
uint32 id=1;
bytes name=2;
bytes pwd=3;
}
message RegisterResponse{
ResultCode result=1;
bool success=2;
}
service UserServiceRpc{
rpc Login(LoginRequest) returns(LoginResponse);
rpc Register(RegisterRequest) returns(RegisterResponse);
}
上面的 .proto 文件末尾还有一个 service 块,用于辅助生成 RPC 框架代码,包括文章一开始图中的 UserServiceRpc 类、UserServiceRpc_stub 类。
UserServiceRpc 类运行在服务器端,他包括各个 RPC 方法(从 .proto 中指定,如这里的 Login、Register)的定义,和一个自动生成的 CallMethod 方法。CallMethod 方法是起到分发的作用,他根据 RPC 方法编号,调用 RPC 方法。
UserServiceRpc_stub 类运行在客户端,protobuf 在该类中自动生成了 RPC 方法在客户端的实现(用户最终使用时,直接调用的就是这些方法)。RPC 方法在客户端的实现很简单,就是调用 UserServiceRpc_stub 的 channel 属性的 CallMethod 方法。通过 channel 完成网络通信。
基于 ProtoBuf,RPC 框架需要做的
在服务端,RPC 框架需要实现一个 Provider 类,可以向该类注册 RPC 服务,该类负责不断进行网络监听,利用 protobuf 提供的遍历序列化网络数据获得客户端想要调用的方法和参数,调用 UserServiceRpc 类的 CallMethod。CallMethod 会调用 RPC 方法在服务端的实现,得到返回值后,Provider 类还要负责把返回值传回去。
在客户端,RPC 框架需要实现 Channel 类(继承自 google::protobuf::RpcChannel),重写其 CallMethod 方法,负责在用户调用 RPC 方法时,作为客户端默默与服务端通信。
用户在服务端需要做的
在服务端,RPC 框架用户需要在服务端自己实现 UserService 类,重写 UserServiceRpc 类中的各个 RPC 方法,赋予其具体实现(这里只需要考虑方法本身的逻辑,不需要考虑网络通信的问题)。
RPC 的局限
很多对 RPC 的介绍中,笼统的说 RPC 框架让用户能像调用本地方法一样调用远程方法。这种说法避开了传递函数参数的困难性。比如调用本地方法时,用户可以传递比较复杂的参数,可能有多个参数。如果真的这样调用远程方法的话,RPC 框架怎么收集并传递各种不同情况的参数呢?
实际上,RPC 框架把这个问题交给了用户,他要求 RPC 方法都有一组统一的参数,从而保证 CallMethod 这种底层通信方法能够统一的传递所有 RPC 方法的参数。只不过利用了 C++ 的 upcast、downcast 的能力,这组参数不限于固定的类型,可以是继承了同一基类的各种派生类。
从 protobuf 的角度也可以印证上面的看法,参考下面连接中的 Services 章,其实 protobuf 生成的 CallMethod 方法和 RPC 方法都有固定的函数签名。