RPC(Remote Procedure Call),主要是帮助我们屏蔽网络编程细节 ,是我们更专注于业务逻辑,实现调用远程方法就像调用本地方法一样。
RPC通信过程如下图所示:
由服务提供者给出业务接口声明,在调用方的程序里面,RPC 框架根据调用的服务接口提前生成动态代理实现类,并通过依赖注入等技术注入到声明了该接口的相关业务逻辑里面。该代理实现类会拦截所有的方法调用,在提供的方法处理逻辑里面完成一整套的远程调用,并把远程调用结果返回给调用方,这样调用方在调用远程方法的时候就获得了像调用本地接口一样的体验。
以下是对RPC的四种角色的解释和说明:
客户端(Client): 服务调用发起方,也称为服务消费者。
客户端存根(Client Stub): 该程序运行在客户端所在的计算机机器上,主要用来存储要调用的服务器的地址,另外,该程序还负责将客户端请求远端服务器程序的数据信息打包成数据包,通过网络发送给服务端Stub程序;其次,还要接收服务端Stub程序发送的调用结果数据包,并解析返回给客户端。
服务端(Server): 远端的计算机机器上运行的程序,其中有客户端要调用的方法。
服务端存根(Server Stub): 接收客户Stub程序通过网络发送的请求消息数据包,并调用服务端中真正的程序功能方法,完成功能调用;其次,将服务端执行调用的结果进行数据处理打包发送给客户端Stub程序。
实际上,如果我们想要在网络中的任意两台计算机上实现远程调用过程,要解决很多问题,比如:
两台物理机器在网络中要建立稳定可靠的通信连接。
两台服务器的通信协议的定义问题,即两台服务器上的程序如何识别对方的请求和返回结果。也就是说两台计算机必须都能够识别对方发来的信息,并且能够识别出其中的请求含义和返回含义,然后才能进行处理。这其实就是通信协议所要完成的工作。
我们来看看RPC具体是如何解决这些问题的,RPC具体的调用步骤图如下:
在上述图中,通过1-10的步骤图解的形式,说明了RPC每一步的调用过程。具体描述为:
1、客户端想要发起一个远程过程调用,首先通过调用本地客户端Stub程序的方式调用想要使用的功能方法名;
2、客户端Stub程序接收到了客户端的功能调用请求,将客户端请求调用的方法名,携带的参数等信息做序列化操作,并打包成数据包。
3、客户端Stub查找到远程服务器程序的IP地址,调用Socket通信协议,通过网络发送给服务端。
4、服务端Stub程序接收到客户端发送的数据包信息,并通过约定好的协议将数据进行反序列化,得到请求的方法名和请求参数等信息。
5、服务端Stub程序准备相关数据,调用本地Server对应的功能方法进行,并传入相应的参数,进行业务处理。
6、服务端程序根据已有业务逻辑执行调用过程,待业务执行结束,将执行结果返回给服务端Stub程序。
7、服务端Stub程序将程序调用结果按照约定的协议进行序列化, 并通过网络发送回客户端Stub程序。
8、客户端Stub程序接收到服务端Stub发送的返回数据,对数据进行反序列化操作, 并将调用返回的数据传递给客户端请求发起者。
9、客户端请求发起者得到调用结果,整个RPC调用过程结束。
在对RPC进行简单介绍之后,我们先看一个简单的RPC服务注册和调用的demo,之后会分别从server和client的核心代码出发,深入到底层去理解RPC库的具体实现,并在学习的过程中思考实现一个RPC协议所需要做的工作以及如何基于RPC基础库进行进一步开发。
服务端代码如下:
package main
import (
"log"
"net"
"net/rpc"
)
func main() {
rpc.RegisterName("HelloService", new(HelloService))
listener, err := net.Listen("tcp", ":8888")
if err != nil {
log.Fatal("ListenTCP error:", err)
}
rpc.Accept(listener)
}
type HelloService struct{}
func (p *HelloService) Hello(request string, reply *string) error {
*reply = "hello:" + request
log.Println("got req", request)
return nil
}
复制代码
调用端代码如下:
package main
import (
"fmt"
"log"
"net/rpc"
)
func main() {
client, err := rpc.Dial("tcp", "localhost:8888")
if err != nil {
log.Fatal("dialing:", err)
}
var reply string
err = client.Call("HelloService.Hello", "RPC", &reply)
if err != nil {
log.Fatal(err)
}
fmt.Println(reply)
}
复制代码
启动server.go 之后启动client.go ,RPC调用成功,控制台分别打印:
// server
got req RPC
// client
hello:RPC
复制代码
接下来我们分别从服务端和客户端两个角度对其原理进行解析。
在代码中,可以看到服务端方法主要包括RegisterName、Listen和Accept, 我将服务端主要的工作流程分为三个步骤:
服务方法注册
监听和参数处理
调用 RPC 方法
首先我们来看注册过程,注册过程中主要调用的方法是server.register方法,其实主要需要注册的就是服务对象和服务方法,接下来通过代码来看是如何为实现的:
相关结构体:
type service struct {// 表示服务的结构体,用于注册服务对象
name string // 服务名
rcvr reflect.Value // 注册服务的结构体实例
typ reflect.Type // 注册服务的结构体类型
method map[string]*methodType // 方法名与方法的映射列表
}
type methodType struct {// 表示方法的结构体,用于注册服务方法
sync.Mutex
method reflect.Method // 方法名
ArgType reflect.Type // 参数类型
ReplyType reflect.Type // 返回值类型
numCalls uint // 调用次数
}
复制代码
注册主要流程:
func (server *Server) RegisterName(name string, rcvr interface{}) error {
return server.register(rcvr, name, true)
}
func (server *Server) register(rcvr interface{}, name string, useName bool) error {
// 1\. 服务相关字段的注册
s := new(service)
s.typ = reflect.TypeOf(rcvr) // 获取实例类型
s.rcvr = reflect.ValueOf(rcvr) // 获取实例本身
// 当Type为指针时Name()返回空字符串,所以要先通过Indirect取指针的值
sname := reflect.Indirect(s.rcvr).Type().Name() // 取结构体类型名
// 是否指定服务名的处理:useName==true默认使用参数中的name
if useName {
sname = name
}
if sname