RPC简介
本地过程调用
// 正常情况下程序的执行和调用情况。例如有如下go语言代码:
package main
import "fmt"
func main() {
var a,b int
a = 1
b = 2
c := Add(a,b)
fmt.Println("计算结果",c)
}
func Add(a int,b int) int{
return a+b
}
在上述的Go语言代码中,我们定义了一个Add方法用于实现两个数相加的功能,在main方法中通过调用Add方法实现了计算两个变量之和的操作。整个过程涉及到变量值入栈,出栈,赋值等操作,最后将出栈的计算结果返回并赋值给c变量。
总结说来,本地程序调用的过程大致可以分为几个步骤和阶段:
// 1.开发者开发好的程序,并进行编译,编译成机器认可的可执行文件。
// 2. 运行可执行文件,调用对应的功能方法,期间会读取可执行文件中国的机器指令,进行入栈,出栈赋值等操作。
// 此时,计算机由可执行程序所在的进程控制。
// 3. 调用结束,所有的内存数据出栈,程序执行结束。计算机继续由操作系统进行控制。
远程过程调用是在两台或者多台不同的物理机器上实现的调用,其间要跨越网络进行调用。因此,我们再想通过前文本地方法调用的形式完成功能调用,就无法实现了,因为编译器无法通过编译的可执行文件来调用远程机器上的程序方法。因此需要采用RPC的方式来实现远端服务器上的程序方法的调用。
RPC技术内部原理是通过两种技术的组合来实现的:本地方法调用 和 网络通信技术。
RPC简介
在上述本地过程调用的例子中,我们是在一台计算机上执行了计算机上的程序,完成调用。随着计算机技术的发展和需求场景的变化,有时就需要从一台计算机上执行另外一台计算机上的程序的需求,因此后来又发展出来了RPC技术。特别是目前随着互联网技术的快速迭代和发展,用户和需求几乎都是以指数式的方式在高速增长,这个时候绝大多数情况下程序都是部署在多台机器上,就需要在调用其他物理机器上的程序的情况。
RPC是Remote Procedure Call Protocol三个单词首字母的缩写,简称为:RPC,翻译成中文叫远程过程调用协议。所谓远程过程调用,通俗的理解就是可以在本地程序中调用运行在另外一台服务器上的程序的功能方法。这种调用的过程跨越了物理服务器的限制,是在网络中完成的,在调用远端服务器上程序的过程中,本地程序等待返回调用结果,直到远端程序执行完毕,将结果进行返回到本地,最终完成一次完整的调用。
需要强调的是:远程过程调用指的是调用远端服务器上的程序的方法整个过程。
rpc和rpc/jsonrpc包提供了对RPC的支持
// rpc构建TCP或HTTP协议之上,底层数据编码使用gob,因为gob编码为golang定义,所以无法支持跨语言调用.
// rpc/jsonrpc构建与TCP协议之上,底层数据编码使用json,可支持跨语言调用
RPC设计组成
RPC技术在架构设计上有四部分组成,分别是:客户端、客户端存根、服务端、服务端存根。
这里提到了客户端和服务端的概念,其属于程序设计架构的一种方式,在现代的计算机软件程序架构设计上,大方向上分为两种方向,分别是:B/S架构、C/S架构。B/S架构指的是浏览器到服务器交互的架构方式,另外一种是在计算机上安装一个单独的应用,称之为客户端,与服务器交互的模式。
由于在服务的调用过程中,有一方是发起调用方,另一方是提供服务方。因此,我们把服务发起方称之为客户端,把服务提供方称之为服务端。以下是对RPC的四种角色的解释和说明:
- 客户端(Client):服务调用发起方,也称为服务消费者。
- 客户端存根(Client Stub):该程序运行在客户端所在的计算机机器上,主要用来存储要调用的服务器的地址,另外,该程序还负责将客户端请求远端服务器程序的数据信息打包成数据包,通过网络发送给服务端Stub程序;其次,还要接收服务端Stub程序发送的调用结果数据包,并解析返回给客户端。
- 服务端(Server):远端的计算机机器上运行的程序,其中有客户端要调用的方法。
- 服务端存根(Server Stub):接收客户Stub程序通过网络发送的请求消息数据包,并调用服务端中真正的程序功能方法,完成功能调用;其次,将服务端执行调用的结果进行数据处理打包发送给客户端Stub程序.
RPC原理及调用步骤
了解完了RPC技术的组成结构我们来看一下具体是如何实现客户端到服务端的调用的。实际上,如果我们想要在网络中的任意两台计算机上实现远程调用过程,要解决很多问题,比如:
- 两台物理机器在网络中要建立稳定可靠的通信连接。
- 两台服务器的通信协议的定义问题,即两台服务器上的程序如何识别对方的请求和返回结果。也就是说两台计算机必须都能够识别对方发来的信息,并且能够识别出其中的请求含义和返回含义,然后才能进行处理。这其实就是通信协议所要完成的工作。
让我们来看看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的由来和RPC整个调用过程。我们可以看到RPC是一系列操作的集合,其中涉及到很多对数据的操作,以及网络通信。因此,我们对RPC中涉及到的技术做一个总结和分析:
-
1、动态代理技术: 上文中我们提到的Client Stub和Sever Stub程序,在具体的编码和开发实践过程中,都是使用动态代理技术自动生成的一段程序。
-
2、序列化和反序列化:
在RPC调用的过程中,我们可以看到数据需要在一台机器上传输到另外一台机器上。在互联网上,所有的数据都是以字节的形式进行传输的。而我们在编程的过程中,往往都是使用数据对象,因此想要在网络上将数据对象和相关变量进行传输,就需要对数据对象做序列化和反序列化的操作。
- 序列化:把对象转换为字节序列的过程称为对象的序列化,也就是编码的过程。
- 反序列化:把字节序列恢复为对象的过程称为对象的反序列化,也就是解码的过程
我们常见的Json,XML等相关框架都可以对数据做序列化和反序列化编解码操作。之前Protobuf协议,这也是一种数据编解码的协议,在RPC框架中使用的更广泛。
RCP定义和使用
定义RPC
定义RPC结构体和方法
// RPC方法必须要有两个参数和返回值error,
// 第一个参数为请求结构体变量,指用于获取客户端提交的参数
// 第二个参数为响应结构体指针变量,用于返回,返回值error用于告知客户端错误信息
Rpcserver
tree rpcserver
rpcserver
├── data
│ └── data.go
├── go.mod
├── main.go
└── service
└── rpc_client.go
go mod init rpcserver
rpcserver/data/data.go
package data
type CalculatorRequest struct {
Left int
Right int
}
// 定义算法服务
type CalculatorResponse struct {
Result int
}
rpcserver/service/rpc_server
package service
import (
"log"
"rpcserver/data"
)
// 定义算法服务
type Calculator struct {
}
// 定义+方法
func (c *Calculator) Add(request *data.CalculatorRequest,response *data.CalculatorResponse) error {
log.Printf("[+] call add method
")
response.Result = request.Left + request.Right
return nil
}
rpcserver/main.go
package main
import (
"log"
"net"
"net/rpc"
"net/rpc/jsonrpc"
"rpcserver/service"
)
func main() {
addr := ":8080"
// 注册服务,未指定服务名称,默认结构体名
//rpc.Register(&service.Calculator{})
// 注册服务,指定服务名称
rpc.RegisterName("Calc",&service.Calculator{})
lister,err := net.Listen("tcp",addr)
if err != nil{
log.Fatal(err)
}
defer lister.Close()
log.Printf("[+] listen on: %s",addr)
for {
conn,err := lister.Accept()
if err != nil{
log.Printf("[-]error client: %s",err.Error())
continue
}
log.Printf("[+] client connected: %s",conn.RemoteAddr())
// 使用例程启动jsonrpc处理客户端请求
go jsonrpc.ServeConn(conn)
}
}
Rpcclient
tree rpcclient
rpcclient
├── data
│ └── data.go
├── go.mod
└── main.go
1 directory, 3 files
rpcclient/data/data.go
package data
type CalculatorRequest struct {
Left int
Right int
}
// 定义算法服务
type CalculatorResponse struct {
Result int
}
rpcclient/main.go
package main
import (
"fmt"
"log"
"net/rpc/jsonrpc"
"rpcclient/data"
)
func main() {
addr := "127.0.0.1:8080"
conn,err := jsonrpc.Dial("tcp",addr)
if err != nil{
log.Fatal(err)
}
defer conn.Close()
// 定义请求对象
request := &data.CalculatorRequest{2,3}
// 定义响应对象
response := &data.CalculatorResponse{}
// 调用远程方法
//err = conn.Call("Calculator.Add",request,response)
err = conn.Call("Calc.Add",request,response)
// 获取结果
fmt.Println(err,response.Result)
}