likes
comments
collection
share

Golang:从0到1手写一个简易RPC框架

作者站长头像
站长
· 阅读数 13

前言

“造轮子”是开发人员提升自己技术水平的一个很好的手段,本文作为自己从0到1手写一个简易的RPC框架学习的一个总结,讲述了我对于RPC框架学习的大致流程,技术选型,并展示最终的代码成果。

什么是RPC

RPC的基本定义

如果我们需要调用远程服务器上的方法时,正常情况需要涉及组装请求+网络传输,而这些代码的编写无疑增加了开发的复杂度,而RPC框架可以使你调用远程服务器的方法时,就和调用本地的方法一样。相当于,RPC为你屏蔽了底层所有网络传输的细节。 所以,一个最简RPC框架,就是可以满足:为你屏蔽网络传输的一切细节,使得开发人员调用远程服务器上的方法时,就和调用本地内存中的方法一样,让使用者不必显式的区分本地调用和远程调用。

RPC架构

Golang:从0到1手写一个简易RPC框架

如图,RPC框架的核心就是这个所谓的“stub”,中文名称叫做“桩”,也可以叫做“proxy”等等。作为开发者而言,你只需要调用client stub的方法,就可以拿到结果,就好像你调用了一个本地方法一样简单,而实际上你所调用的方法实现,却是在一台远程服务器上。

// 我们只需要这样调用一个方法,就可以拿到返回结果,完全没有感知到服务提供者是在另一台服务器上
res, err := clientStub.GetInfoById(123)
...

那么这个stub究竟该如何生成,就是RPC框架实现的核心了。

“stub”该怎么实现

RPC框架中,stub的实现,本质上是采用了一种设计模式:代理模式。 代理模式为目标类生成一个代理类,由代理类来实现针对目标类的控制,可以用在权限验证、风险控制、调用链跟踪等等很多场景中。

也就是说,我们需要给服务端真正提供方法实现的“被代理类”生成一个“代理类”,这个“代理类”是放在客户端的,代理类需要实现含有用户定义的方法的接口,之后,客户端仅仅需要和这个RPC框架生成的“代理类”进行交流,即可拿到结果,而由代理类去和远程服务器上的“被代理类”进行跨主机交流。

那么,代理模式,在现有成熟的RPC框架中,是怎么实现的呢?

调研

dubbo

dubbo作为Java实现的一款RPC框架,采用的是:“动态代理”来生成RPC的桩。也就是,dubbo会根据用户定义的interface,动态生成一个代理实例。其原理是: Java语言在编译之后,会生成一堆.class文件,这些文件具有固定的格式,本质上就是对你所写的代码的另一种描述。而JVM在运行时可以读取这些.class文件,并加载对应的实例。 动态代理的本质,就是按照.class文件的格式,来生成class文件,这样就可以不用经过源代码编译的阶段,在运行时动态加载一个新的实例了。

grpc

grpc采用的则是:代码生成策略。本质上就是需要你先编写IDL文件,之后根据IDL文件生成目标源代码文件,这些源代码文件就是用于描述生成的桩的,之后,这些生成的源代码会跟着一起被编译。

思路&&设计

分析

因为我们是使用golang来实现RPC框架,动态代理是Java语言独有的特性,go根本不支持动态代理。也就是说:go不可能在运行时,凭空出现一个新的类,在编译时有哪些,运行时就有哪些。 所以,这条思路显然不合适。

其次,代码生成策略需要涉及IDL的编写,之后根据IDL生成目标源代码文件,我目前还没有调研grpc是如何实现的,但是如果让我来做,我可能会按照 模板+数据 的方式来生成代码,也就是将具体的数据填入模板中。

决策

既然go没法在运行时生成一个新的类,那么,我们可不可以在运行时不生成一个新的类,而是篡改掉已有类的内部实现呢,也就是说,你dubbo是在运行时为interface新生成一个代理类,那么我用go,可不可以不新生成一个编译时没有的类,而是在运行时篡改掉一个已有类的内部实现呢?显然是可以的。

type ClientStub struct {
GetInfo func(ctx context.Context, req *GetInfoReq) (*GetInfoResp, error)
}

如图,我们定义了一个结构体,结构体内有一个func类型的成员变量,我们可以把这个结构体理解为一个stub,里面有一个方法,是这个stub下支持被调用的方法,这个方法规定了入参和出参。

之后,我们可以通过反射,为这个方法类型的成员变量注入调用逻辑,也就是说,通过反射,为这个方法注入:序列化->网络传输->拿到结果->反序列化,这样一个过程。

这样,客户端只需要做的操作就是:

// 定义stub
type ClientStub struct {
GetInfo func(ctx context.Context, req *GetInfoReq) (*GetInfoResp, error)
}

client := ClientStub{}

// 初始化stub,在这里使用反射,为方法成员变量注入调用逻辑
stub, err := InitStub(&client)

// 调用方法,拿到返回结果
res, err := stub.GetInfo(ctx, req)

协议设计

所有的网络协议都是分为协议头和协议体两部分,协议头用于放置一些元数据,以及解析协议体所依赖的数据,协议体一般就是放置请求数据了。所以毫无疑问,我们的协议也会分为协议头和协议体两个部分。例如dubbo的协议结构如下:

Golang:从0到1手写一个简易RPC框架

我最终的自定义协议如下:

type Request struct {
   // 头部长度
   HeadLength uint32
   // 消息体长度
   BodyLength uint32
   // 消息id 多路复用使用
   MessageId uint32
   // 版本
   Version byte
   // 压缩算法
   Compressor byte
   // 序列化协议
   Serializer byte
   // ping探活
   Ping byte
   // 服务名
   ServiceName string
   // 方法名
   MethodName string
   // 元数据 可扩展
   Meta map[string]string 
   // 消息体
   Data []byte    
}

type Response struct {
   // 头部长度
   HeadLength uint32
   // 消息体长度
   BodyLength uint32
   // 消息id 多路复用使用
   MessageId uint32
   // 版本
   Version byte
   // 压缩算法
   Compressor byte
   // 序列化协议
   Serializer byte
   // pong探活
   Pong byte
   // 错误信息 可以是业务error,也可以是框架error
   Error []byte
   // 协议体
   Data []byte
}

总结

以上就是实现一个最简RPC框架的调研,以及思考,设计过程。我实现的仅仅是一个最基础的RPC框架,在工业界,一个生产级别的RPC框架还需要支持很多的功能,这些功能的本质都是为了应对生产环境的大流量特点以及保障服务稳定性健壮性而必须具备的,例如服务发现,负载均衡,熔断限流,异常重试,链路追踪,路由分组等等,这些都是我在未来会不断学习和完善的点。

结语

上述基础RPC框架完整代码的地址是 Zhang-hs-home/RPC: A single RPC framework based on Go (github.com)。 这个项目目前已经支持:

  • 采用自定义协议,分为协议头和协议体,手动对协议进行编码和解码。
  • 基于TCP进行网络通信。
  • 支持轻松的扩展序列化协议作用于协议体,源代码已支持json,protobuf协议。
  • 采用连接池管理客户端连接。
  • 采用ping探活检测连接的健康状态,如果连接池中的连接有问题,则会丢弃掉连接。

如果对你有帮助的话,期待你能够点亮star,也期待你提出宝贵的建议,如有错误,也欢迎指正。