从go原生rpc库源码探察rpc机制 目录
从go原生rpc库源码探察rpc机制
一、调用过程比较
1.1第三方库依赖本地调用:
1.2自己的写的方法本地调用:
1.3Rpc调用
【golang|从go原生rpc库源码探察rpc机制】二、Go语言中Rpc库实现原理
2.1服务端服务注册:
2.2客户端发送请求:
2.3服务端处理请求
2.4客户端处理响应:
一、调用过程比较
1.1第三方库依赖本地调用: 对于传统方法来说,本地调用主要是通过链接器从库中提取出来,然后链接器再将它链接到目标程序之中。普通方法执行系统调用,但是本身就是通过将参数压入执行返回的常规方式,调用方是并不知道被调用方法的具体实现和行为。
1.2自己的写的方法本地调用:
- 编译器查看对象的声明类型和方法名。
- 编译器将查看调用方法是提供方的参数类型。
- 如果是private、static、final方法或者构造方法,编译器会准确的知道应该调用那个方法,这种调用方式成为静态绑定,与此对应的是,调用方法依赖于隐式参数的实际类型,并且在运行时实现动态绑定。
- 当程序运行时,并且采用动态绑定的调用方法时,虚拟机一定会调用与参数所引用的对象的实际类型最合适的那个类的方法,通过字节码的一些逻辑~
对于远程过程调用也是通过类似本地调用的方式来得到透明性,通过在本地库中放入被调用方法的接口形式,称为客户端存根,具体流程如图1,与本地调用最大的最大不同就是rpc不要求本地系统提供返回数据,而是将参数序列化打包成为消息,进行网络传输,发送给服务端,发送完成之后客户端进入阻塞,等待服务器的响应消息。
服务端收到消息之后,服务器上的操作系统会检查本地的服务器存根,若存根中存在则将网络打包过来的参数消息请求转化为进行本地方法调用,最后将得到的结果以打包的形式返回给调用方。
文章图片
Rpc实现机制和实现过程
RPC调用和本地方法调用最大的不同就是经过了网络的传输和参数的打包和解码。所以RPC的实现过程中,需要主要考虑参数传递,通信协议制定,出错和超时处理等。
在参数传递方面,需要将参数从调用方传递给被调用方。传递的参数应包括调用方法和请求参数,并且避免引用传递,引用传递在不同的系统上指向是不一样的,传递引用通常来讲没有意义。同时需要提前规范好消息传递的统一序列化格式,对于分布式系统来讲,不同的机器上数据格式会存在不同,为了实现分布式系统中异构系统通信,所以我们需要定义统一的数据格式,定义一个标准来对所有的数据格式进行编码。
在通信协议方面,协议设计过程中,应避免无效的字段,同时需要支持CRC数据校验、安全校验等功能,考虑协议的升级机制,防止协议升级带来全部重构。命令定义和命令处理器是,需要同时定义负载命令和通信命令,并且定义命令处理器,保证命令和命令处理器的对应关系,以便在处理阶段走到正确的处理命令,同时定义命令序列化协议,如thrift、json等。通信模式选择过程中,因根据不同的业务场景选择不同的通信模式,通信模式有oneway、Sync、Future、Callback模式。
远程调用的出错率比本地方法更大,远程调用一次性成功是很难实现的。由于根据网络或服务器原因都会导致出错和超时,RPC系统需要提供至少一次或最多一次的语义和幂等语义。
二、Go语言中Rpc库实现原理 服务端的rpc代码主要分为两个模块,第一个模块是服务方法的注册,通过将本地提供可被调用的方法保留到map中,记录服务端存根;第二个模块书处理网络调用,通过解析网络请求中的参数,定位到本地方法,指向本地方法并返回调用结果。
2.1服务端服务注册: 第一个模块服务端方法注册,主要步骤如图。
文章图片
文章图片
首先服务端需要注册服务,注册服务的入口为rpc.register方法,rpc.register则调用DefaultServer.Register方法,DefaultServer的Register方法主要检查服务是否是否已经被注册过,检查方法和服务是否是暴露的,并且通过反射获取需要注册服务的服务名和类型。同时方法内调用suitableMethods方法通过反射的到可被调用方法的方法名、入参、出参并存入map结构的methods中。最后根据返回将服务的所有方法作为value。服务名作为key,整体服务存入serviceMap中,形成服务器存根。整体调用链如图:
文章图片
2.2客户端发送请求: 客户端每次进行调用都会生成一个Call对象,并且维护一个map,并将序列seq作为key保存在其中,当收到服务端响应的时,会将响应的序列seq与map中的key比较,定位到对应的情求做对应的处理。客户端发送请求主要有异步/同步调用、请求参数打包、接收服务器响应三个步骤组成,发送请求的主要流程如图所示:
文章图片
客户端发送请求分为异步调用和同步调用,实现方式主要通过定义chan中的缓冲区大小来实现的,同步缓冲区大小为1,异步缓冲区默认大小为10。封装请求为一个call对象,本地保留映射,通过seq来形成唯一标识,发送call对象进行序列化实现远程调用,主要流程如图:
文章图片
2.3服务端处理请求 处理请求的方法主要为accept方法,通过无线循环一直等待新的请求到来。当请求到来的时候就会开启一个新的线程处理请求,新的线程主要是从请求中进行协议的解码。解码之后读取请求头和请求体中的服务、方法名、参数等信息,调用本地方法,调用本地方法之后通过wirteResponse方法传递返回值给客户端,服务端处理请求流程如图,代码逻辑如图。
文章图片
文章图片
图6:服务端处理客户端请求流程
文章图片
2.4客户端处理响应: 客户端在发起请求生成client的时候,就会开启input线程,时刻监听调用的返回值,若返回则进行处理,首先进行解码,解码之后读取请求信息,通过seq移除成功的call,最后将返回体赋值给call.reply,并通过call.done中是否有元素进行告知,主要流程如图,Go原生rpc库中逻辑如图。
文章图片
文章图片
图8:Go原生rpc的客户端处理响应流程
总结
随着微服务的快速发展,分布式系统中不同系系统直接的信息交互越来越频繁,本文对go语言中原生rpc库进行解读和分析,帮助深入了解rpc原理和用法。当然go中rpc库服务发现的功能还是有所欠缺,没有达到即用的水平。只是实现了基本的网络通信,解码打包、超时出错处理等方面。后续需要进一步学习rpc的相关封装框架,弥补原生的不足。
参考《Go语言高并发与微服务实战》
推荐阅读
- Go|Docker后端部署详解(Go+Nginx)
- GO|GO,GO,GO!
- 【golang】leetcode中级-字母异位词分组&无重复字符的最长子串
- 彻底理解Golang Map
- RPC基础概念问答
- kratos线上开源年会它来啦~
- 深入浅出 Golang 资源嵌入方案(go-bindata篇)
- 深入浅出 Golang 资源嵌入方案(前篇)
- golang 经典案例总结