前言
“造轮子”是开发人员提升自己技术水平的一个很好的手法,本文作为自己从0到1手写一个简易的RPC结构学习的一个总结,讲述了我关于RPC结构学习的大致流程,技术选型,并展示终究的代码效果。
什么是RPC
RPC的底子界说
假如咱们需求调用长途服务器上的办法时,正常状况需求触及拼装请求+网络传输,而这些代码的编写无疑增加了开发的复杂度,而RPC结构可以使你调用长途服务器的办法时,就和调用本地的办法相同。相当于,RPC为你屏蔽了底层全部网络传输的细节。 所以,一个最简RPC结构,便是可以满意:为你屏蔽网络传输的全部细节,使得开发人员调用长途服务器上的办法时,就和调用本地内存中的办法相同,让运用者不用显式的区分本地调用和长途调用。
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,在这里运用反射,为办法成员变量注入调用逻辑
err := InitStub(&client)
// 调用办法,拿到回来成果
res, err := client.GetInfo(ctx, req)
协议规划
可以看到,无论是http协议仍是TCP协议,我了解到的全部的网络协议都是分为协议头和协议体两部分,协议头用于放置一些元数据,以及解析协议体所依靠的数据,协议体一般便是放置请求数据了。所以毫无疑问,咱们的协议也会分为协议头和协议体两个部分。例如dubbo的协议结构如下:
我终究的自界说协议如下:
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结构明显也需求支撑这个功用。所以咱们界说了通用的序列化办法,全部的序列化协议都需求完成咱们界说的办法。
type Serializer interface { // 序列化协议仅仅用来序列化协议体的,不触及头部
Code() byte
Encode(val interface{}) ([]byte, error)
Decode(data []byte, val interface{}) error
}
衔接健康检测
咱们选用了衔接池来保管客户端到服务端树立的TCP衔接,那么这个衔接在不运用的时分,会一直放在池子里,就有或许由于各种原因导致衔接失效,当咱们再次从池中拿出这个现已损坏的衔接进行数据传输时,就会有问题,而假如选用TCP自带的keep-alive机制去检测衔接的健康状况,需求至少两个小时才干发现衔接的反常,所以咱们需求完成一个应用层的衔接健康检测机制。我的完成实质上便是模仿TCP自带的健康检测机制,发送一个含有很少数据的报文,服务端检测到这是一个心跳报文,也会回复一个含有很少数据的心跳回复,当客户端可以接收到这个心跳回复,就标明衔接是健康的。
所以咱们在每次从衔接池获取一个衔接时,都要先运用这个衔接ping一下服务端,假如这个衔接是健康的,那就可以被运用,反之就需求丢掉这个衔接。
总结
以上便是完成一个最简RPC结构的调研,以及思考,规划进程。我完成的仅仅是一个最根底的RPC结构,在工业界,一个生产级别的RPC结构还需求支撑许多的功用,这些功用的实质都是为了应对生产环境的大流量特色以及保证服务稳定性健壮性而必须具备的,例如服务发现,负载均衡,熔断限流,反常重试,链路追寻,路由分组等等,这些都是我在未来会不断学习和完善的点。
结语
上述根底RPC结构完好代码的地址是 Zhang-hs-home/RPC: A single RPC framework based on Go (github.com)。 这个项目现在现已支撑:
- 选用自界说协议,分为协议头和协议体,手动对协议进行编码和解码。
- 根据TCP进行网络通信。
- 支撑轻松的扩展序列化协议作用于协议体,源代码已支撑json,protobuf协议。
- 选用衔接池办理客户端衔接。
- 选用ping探活检测衔接的健康状态,假如衔接池中的衔接有问题,则会丢掉掉衔接。
假如对你有帮助的话,期待你可以点亮star,也期待你提出宝贵的建议,如有过错,也欢迎指正。