rpc的正确打开方式|读懂Go原生net/rpc包

网友投稿 730 2022-11-27

rpc的正确打开方式|读懂Go原生net/rpc包

rpc的正确打开方式|读懂Go原生net/rpc包

前言

最近在阅读字节跳动开源RPC框架Kitex的源码,分析了​​如何借助命令行​​,由一个IDL文件,生成​​client​​​和​​server​​​的脚手架代码,也分析了​​Kitex的日志组件klog​​。当然Kitex还有许多其他组件:服务注册、发现、负载均衡、熔断、限流等等,后续我也会继续分析。

我希望借助这篇文章,用尽可能少的语言,配合分析Go原生​​net/rpc​​包的部分核心代码,帮助你贯通RPC的知识,梳理RPC的运作流程,让你对RPC有一个比较全面的认识。

以此为基础,将有助于你在阅读其他开源RPC框架源码时,对比发掘开源RPC框架具体做了哪些提高。

RPC的流程

远程过程调用 (Remote Procedure Call,RPC) 是一种计算机通信协议。允许运行在一台计算机的程序调用另一个地址空间的子程序(一般是开放网络中的一台计算机),而程序员就像调用调用本地程序一样,无需额外做交互编程。

假设你要调用一个​​Add(a int, b int) int​​方法,实现求和功能,但是这个方法部署在另一台机器上,该如何调用?

这就是一次RPC的流程,甚至和HTTP请求/响应流程很像,眼下我先侧重于介绍RPC的概念,以后会介绍其与HTTP的区别。

并且这里暂时没有涉及所谓的服务注册、发现、负载均衡、熔断、限流等字眼,这些都是一个成熟的RPC框架应该具备的功能组件,用于确保一个RPC框架的高可用,但是却不是一个RPC框架所必需的。

RPC协议本质上定义了一种通信的流程,而具体的实现技术是没有约束的,每一种RPC框架都有自己的实现方式,比如你可以规定自己的RPC请求/响应包含消息头和消息体,使用​​gob/json/pb/thrift​​来序列化/反序列化消息内容,使用​​socket/http2​​进行网络通信,只要​​client​​和​​server​​消息的发送和解析能对应即可。希望读者仔细体会——“约定”这个概念,这将贯穿始终。

分析net/rpc

先讲解一下流程图中的序列化和网络传输部分,这是RPC的核心。

消息编码/解码(序列化)

上面的RPC通信流程图,其中很重要的一环就是消息的编解码,消息只有序列化之后,才能高效地参与网络传输。通过实现上图​​net/rpc​​​包定义的接口,可以指定使用的编解码方式,比如​​net/rpc​​​包默认使用了​​gob​​二进制编码:

服务端负责序列化的结构​​gobServerCodec​​​的实现了​​ServerCodec​​​接口,服务端需要编解码消息的地方,都会调用​​gobServerCodec​​​的对应方法(客户端也是类似的实现,也是一样使用​​gob​​编解码)。

消息的网络传输

消息序列化之后,是需要用于网络传输的,涉及到客户端与服务端的通信方式。

这是服务端的接受链接的逻辑,和大部分网络应用相同,​​server​​​监听了一个​​ip:port​​

,然后​​accept​​​一个连接之后,会开启一个​​go​​协程处理请求与响应。

这是客户端发起请求的方式,也印证了​​socket​​网络编程的通信模型。

理解了RPC的各个流程之后,就能梳理清楚RPC框架的各种组件是作用在哪个层面的,例如Kitex的网络库​​netpoll​​,虽然我未曾看过其源码实现,但是有理由猜测其是在网络通信/传输部分做了提高。

Server端的设计

这是​​service​​的结构,可以看到一个服务通过​​Map​​可以绑定多个名称的方法,提供调用,且对应​​service​​需要提前注册到服务端,这样在客户端请求达到时才能准确调用。

服务注册主要参数是​​serviceName​​​和​​service​​实体。

​​reflect.xxx()​​:主要的工作就是通过反射的机制,解析所绑定的服务的名称、类型等。​​suitableMethods()​​​:解析一个​​service​​​绑定的所有​​method​​。​​serviceMap.LoadOrStore()​​​:将​​service​​​注册到服务端​​server​​的Map,如下是​​Server​​的结构:

Client端的设计

这是​​Client​​的结构:

​​codec​​:编解码的具体实现。​​seq​​:RPC的序列号,每发起一个就计数增加,加入Map,且完成或失败后从Map中移除。​​pending​​​:配合​​seq​​工作的Map。

这是客户端具体发起一次RPC请求的过程,当然一次具体的RPC请求可以是同步的,也可以是异步的:

​​client.Go()​​是异步的。​​client.Call()​​​是同步的,且其内部就是调用了​​client.Go()​​​,但是因为其调用之后,在调用完成之前,会被阻塞在​​chan​​上,因此后续的RPC请求必须等待发送。

小结

到此为止我们粗浅的分析了​​net/rpc​​的一些核心源码,借此梳理了RPC的工作流程,主要包括:

RPC的编解码(序列化)协议选择RPC的网络通信/传输模型(Socket编程)RPC的请求发起/响应接受(同步/异步)

RPC的功能组件

一个成熟的RPC框架只实现基本的通信功能是不够的,否则它将十分的脆弱,没有任何应对服务宕机的能力,在高并发场景下也难堪重任,因此需要增加很多的功能组件来提高服务的可靠性:

超时控制|请求重试|负载均衡|熔断器|限流器|日志|监控|链路追踪|...

(Go原生​​net/rpc​​包也有很多提高可靠性的设计,本文没有过多展开)

结束语

这篇文章,我借助Go原生​​net/rpc​​包的部分核心源码,梳理了RPC的工作流程,试图帮助你建立RPC的全局观念,希望你明白,RPC框架是对RPC通信流程的具体实现,每一个框架为提高自身的可靠性,又延伸出了多种功能组件。

后续的文章我也将继续分析字节跳动开源RPC框架Kitex的核心组件源码,共勉。

版权声明:本文内容由网络用户投稿,版权归原作者所有,本站不拥有其著作权,亦不承担相应法律责任。如果您发现本站中有涉嫌抄袭或描述失实的内容,请联系我们jiasou666@gmail.com 处理,核实后本网站将在24小时内删除侵权内容。

上一篇:我的sql没问题为什么还是这么慢|MySQL加锁规则
下一篇:如何查看JVM使用的默认的垃圾收集器
相关文章

 发表评论

暂时没有评论,来抢沙发吧~