RPC 协议构建于 TCP、UDP 或者是 HTTP 之上,允许开发人员直接调用另一台计算机上的程序,而开发人员无需额外地为这个调用过程编写网络通信相关代码,使得开发网络分布式类型的应用程序更加容易。
Go语言的标准库提供了 RPC 框架和不同的 RPC 实现。
远程过程调用(Remote Procedure Call,简称 RPC)是一个计算机通信协议。该协议允许运行于一台计算机的程序调用另一台计算机的子程序,而开发人员无需额外地为这个交互作用编程。如果涉及的软件采用面向对象编程,那么远程过程调用亦可称作远程调用或远程方法调用。
通俗的来讲就是,RPC 允许跨机器、跨语言调用计算机程序。例如我们用Go语言写了一个获取用户信息的方法 getUserInfo,并把Go语言程序部署在阿里云服务器上面,另外我们还有一个部署在腾讯云上面的 php 项目,需要调用Go语言的 getUserInfo 方法获取用户信息,php 跨机器调用 Go 方法的过程就是 RPC 调用。
RPC 的工作流程如下图所示:
在Go语言中实现 RPC 非常简单,有封装好的官方包和一些第三方包提供支持。Go语言中 RPC 可以利用 tcp 或 http 来传递数据,可以对要传递的数据使用多种类型的编解码方式。
Go语言的 net/rpc 包使用 encoding/gob 进行编解码,支持 tcp 或 http 数据传输方式,由于其他语言不支持 gob 编解码方式,所以使用 net/rpc 包实现的 RPC 方法没办法进行跨语言调用。
此外,Go语言还提供了 net/rpc/jsonrpc 包实现 RPC 方法,JSON RPC 采用 JSON 进行数据编解码,因而支持跨语言调用。但目前的 jsonrpc 包是基于 tcp 协议实现的,暂时不支持使用 http 进行数据传输。
除了Go语言官方提供的 rpc 包,还有许多第三方包为在Go语言中实现 RPC 提供支持,大部分第三方 rpc 包的实现都是使用 protobuf 进行数据编解码,根据 protobuf 声明文件自动生成 rpc 方法定义与服务注册代码,所以在Go语言中可以很方便的进行 rpc 服务调用。
rpc 包提供了通过网络或其他 I/O 连接对一个对象的导出方法的访问。服务端注册一个对象,使它作为一个服务被暴露,服务的名字是该对象的类型名。注册之后,对象的导出方法就可以被远程访问。服务端可以注册多个不同类型的对象(服务),但注册具有相同类型的多个对象是错误的。
只有满足如下标准的方法才能用于远程访问,其余方法会被忽略:
下面的示例演示了Go语言 net/rpc 包实现 RPC 方法,使用 http 作为 RPC 的载体,通过 net/http 包监听客户端连接请求。
服务端代码如下:
package main
import (
"errors"
"fmt"
"log"
"net"
"net/http"
"net/rpc"
"os"
)
// 算数运算结构体
type Arith struct {
}
// 算数运算请求结构体
type ArithRequest struct {
A int
B int
}
// 算数运算响应结构体
type ArithResponse struct {
Pro int // 乘积
Quo int // 商
Rem int // 余数
}
// 乘法运算方法
func (this *Arith) Multiply(req ArithRequest, res *ArithResponse) error {
res.Pro = req.A * req.B
return nil
}
// 除法运算方法
func (this *Arith) Divide(req ArithRequest, res *ArithResponse) error {
if req.B == 0 {
return errors.New("除以零")
}
res.Quo = req.A / req.B
res.Rem = req.A % req.B
return nil
}
func main() {
rpc.Register(new(Arith)) // 注册rpc服务
rpc.HandleHTTP() // 采用http协议作为rpc载体
lis, err := net.Listen("tcp", "127.0.0.1:8080")
if err != nil {
log.Fatalln("致命错误: ", err)
}
fmt.Fprintf(os.Stdout, "%s", "开始连接")
http.Serve(lis, nil)
}
服务端程序运行之后将会监听本地的 8080 端口,下面我们再来看一下客户端程序,用于连接服务端并实现 RPC 方法调用,完整代码如下:
package main
import (
"fmt"
"log"
"net/rpc"
)
// 算数运算请求结构体
type ArithRequest struct {
A int
B int
}
// 算数运算响应结构体
type ArithResponse struct {
Pro int // 乘积
Quo int // 商
Rem int // 余数
}
func main() {
conn, err := rpc.DialHTTP("tcp", "127.0.0.1:8080")
if err != nil {
log.Fatalln("连接错误: ", err)
}
req := ArithRequest{11, 2}
var res ArithResponse
err = conn.Call("Arith.Multiply", req, &res) // 乘法运算
if err != nil {
log.Fatalln("算术误差: ", err)
}
fmt.Printf("%d * %d = %d\n", req.A, req.B, res.Pro)
err = conn.Call("Arith.Divide", req, &res)
if err != nil {
log.Fatalln("算术误差: ", err)
}
fmt.Printf("%d / %d, 商是 %d, 余数是 %d\n", req.A, req.B, res.Quo, res.Rem)
}
运行结果如下:
上面的例子演示了使用 net/rpc 包实现 RPC 的过程,但是没办法在其他语言中调用上面例子所实现的 RPC 方法。
Go语言提供了 net/rpc/jsonrpc 包,用于提供基于 json 编码的 RPC 支持。在不指定编码协议时,默认采用 Go 特有的 gob 编码协议。但是其他语言一般不支持 Go 的 gob 协议,所以如果需要跨语言的 RPC 调用,需要采用通用的编码协议。
服务端部分的代码如下所示:
package main
import (
"errors"
"fmt"
"log"
"net"
"net/rpc"
"net/rpc/jsonrpc"
"os"
)
// 算数运算结构体
type Arith struct {
}
// 算数运算请求结构体
type ArithRequest struct {
A int
B int
}
// 算数运算响应结构体
type ArithResponse struct {
Pro int // 乘积
Quo int // 商
Rem int // 余数
}
// 乘法运算方法
func (this *Arith) Multiply(req ArithRequest, res *ArithResponse) error {
res.Pro = req.A * req.B
return nil
}
// 除法运算方法
func (this *Arith) Divide(req ArithRequest, res *ArithResponse) error {
if req.B == 0 {
return errors.New("除以零")
}
res.Quo = req.A / req.B
res.Rem = req.A % req.B
return nil
}
func main() {
rpc.Register(new(Arith)) // 注册rpc服务
lis, err := net.Listen("tcp", "127.0.0.1:8080")
if err != nil {
log.Fatalln("致命错误: ", err)
}
fmt.Fprintf(os.Stdout, "%s", "开始连接")
for {
conn, err := lis.Accept() // 接收客户端连接请求
if err != nil {
continue
}
go func(conn net.Conn) { // 并发处理客户端请求
fmt.Fprintf(os.Stdout, "%s", "新连接接入\n")
jsonrpc.ServeConn(conn)
}(conn)
}
}
上述服务端程序启动后,将会监听本地的 8080 端口,并处理客户端的 tcp 连接请求。下面我们再来实现一个客户端程序来连接上述服务端并进行 RPC 调用,完整代码如下:
package main
import (
"fmt"
"log"
"net/rpc/jsonrpc"
)
// 算数运算请求结构体
type ArithRequest struct {
A int
B int
}
// 算数运算响应结构体
type ArithResponse struct {
Pro int // 乘积
Quo int // 商
Rem int // 余数
}
func main() {
conn, err := jsonrpc.Dial("tcp", "127.0.0.1:8080")
if err != nil {
log.Fatalln("连接错误: ", err)
}
req := ArithRequest{11, 3}
var res ArithResponse
err = conn.Call("Arith.Multiply", req, &res) // 乘法运算
if err != nil {
log.Fatalln("算术误差: ", err)
}
fmt.Printf("%d * %d = %d\n", req.A, req.B, res.Pro)
err = conn.Call("Arith.Divide", req, &res)
if err != nil {
log.Fatalln("算术误差: ", err)
}
fmt.Printf("%d / %d, 商是 %d, 余数是 %d\n", req.A, req.B, res.Quo, res.Rem)
}
运行结果如下: