文章目录
RPC简介
什么是rpc
rpc(remote procedure call)远程进程调用,例如:在分布式或者微服务架构中,服务器A想调用服务器B中的一个方法,由于不在同一个内存空间,不能直接调用,需要通过网络来表达调用的语义和调用的数据。
用通俗易懂的语言描述就是:RPC允许跨机器、跨语言调用计算机程序方法。打个比方,我用go语言写了个获取用户信息的方法getUserInfo,并把go程序部署在阿里云服务器上面,现在我有一个部署在腾讯云上面的php项目,需要调用golang的getUserInfo方法获取用户信息,php跨机器调用go方法的过程就是RPC调用。
rpc的工作原理
RPC 工具使用户看起来好像客户端直接调用位于远程服务器程序中的过程。客户端和服务器都有自己的地址空间;也就是说,每个都有自己的内存资源分配给过程使用的数据。下图说明了 RPC 体系结构:
如图所示,客户端应用程序调用本地存根过程,而不是实现该过程的实际代码。 存根是编译的,并与客户端应用程序链接。 客户端存根代码,而不是包含实现远程过程的实际代码:
- 从客户端地址空间检索所需的参数。
- 根据需要将参数转换为标准 NDR 格式,以便通过网络传输。
- 调用 RPC 客户端运行时库中的函数,将请求及其参数发送到服务器。
服务器执行以下步骤来调用远程过程:
- 服务器 RPC 运行时库函数接受请求并调用服务器存根过程。
- 服务器存根从网络缓冲区检索参数,并将其从网络传输格式转换为服务器所需的格式。
- 服务器存根调用服务器上的实际过程。
然后,远程过程将运行,可能生成输出参数和返回值。 远程过程完成后,类似的步骤序列会将数据返回到客户端。
- 远程过程将其数据返回到服务器存根。
- 服务器存根将输出参数转换为通过网络传输所需的格式,并将其返回到 RPC 运行时库函数。
- 服务器 RPC 运行时库函数将网络上的数据传输到客户端计算机。
客户端通过网络接受数据并将其返回到调用函数来完成该过程。
- 客户端 RPC 运行时库接收远程过程返回值,并将其返回到客户端存根。
- 客户端存根将数据从其 NDR 转换为客户端计算机使用的格式。 存根将数据写入客户端内存,并将结果返回到客户端上的调用程序。
- 调用过程继续,就像在同一台计算机上调用了该过程一样。
运行时库由两个部分提供:一个导入库,该库与应用程序和 RPC 运行时库链接,该库作为动态链接库实现, (DLL) 。
服务器应用程序包含对服务器运行时库函数的调用,这些函数注册服务器的接口并允许服务器接受远程过程调用。 服务器应用程序还包含客户端应用程序调用的应用程序特定的远程过程。
golang实现RPC的方式
方式一:net/rpc库
rpc服务端实现
使用golang官方的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("divide by zero")
}
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:8090")
if err != nil {
log.Fatalln("fatal error: ", err)
}
fmt.Fprintf(os.Stdout, "%s", "start connection")
http.Serve(lis, nil)
}
rpc客户端实现
上述服务端程序运行后,将会监听本地的8090端口,我们可以实现一个客户端程序,连接服务端并实现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:8090")
if err != nil {
log.Fatalln("dailing error: ", err)
}
req := ArithRequest{
9, 2}
var res ArithResponse
err = conn.Call("Arith.Multiply", req, &res) // 乘法运算
if err != nil {
log.Fatalln("arith error: ", 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("arith error: ", err)
}
fmt.Printf("%d / %d, quo is %d, rem is %d\n", req.A, req.B, res.Quo, res.Rem)
}
运行结果
方式二:net/rpc/jsonrpc库
虽然使用net/rpc
实现RPC的过程,但是没办法在其他语言中调用上面例子实现的RPC方法。所以接下来的例子我们演示一下使用net/rpc/jsonrpc
库实现RPC方法,此方式实现的RPC方法支持跨语言调用。
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("divide by zero")
}
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:8090")
if err != nil {
log.Fatalln("fatal error: ", err)
}
fmt.Fprintf(os.Stdout, "%s", "start connection")
for {
conn, err := lis.Accept()