RPC

109 阅读4分钟

RPC(Remote Procedure Call),即远程过程调用。它允许像调用本地服务一样调用远程服务。它属于 C/S 模式,一般都是由客户端发起请求,服务端响应。RPC 是为了解决类似远程、跨内存空间、的函数/方法调用的。

我理解的 RPC 是一种思想,一种调用方式,网上经常说的 “为什么有了 HTTP 还要有 RPC?” 我认为完全就是一个不合理的问题,HTTP 是一个协议,跟 RPC 完全是两个东西。实现 RPC 其实也可以使用 HTTP 协议,正如前后端交互基本都是基于 RESTful API ,这也是一种远程调用。

RPC的核心有两个:通信协议序列化。在 HTTP2 之前,一般采用自定义 TCP 协议的方式进行通信,HTTP2 出来后,也有采用该协议的,比如流行的 gRPC,也有自己造轮子的,如 thrift 。

下面我为你总结下 RPC 调用的流程:

  • 客户端(Client)调用客户端存根(Client Stub),同时把参数传给客户端存根;
  • 客户端存根将参数打包编码,并通过系统调用发送到服务端;
  • 客户端本地系统发送信息到服务器;
  • 服务器系统将信息发送到服务端存根(Server Stub);
  • 服务端存根解析信息,也就是解码;
  • 服务端存根调用真正的服务端程序(Sever);
  • 服务端(Server)处理后,通过同样的方式,把结果再返回给客户端(Client)。

RPC 调用常用于大型项目,也就是我们现在常说的微服务,而且还会包含服务注册、治理、监控等功能,是一套完整的体系。

如何在 Go 语言中实现 RPC

Go 语言为我们内置了 net/rpc 包来让我们实现 rpc。

接下来我们用一个加法来演示 rpc 包的基本使用。

// 自定义服务 A
type ServiceA struct{}

// 方法的参数
type Args struct {
   A, B int
}

// 我们要暴露的方法
func (s *ServiceA) Add(args *Args, resp *int) error {
   *resp = args.A + args.B
   return nil
}

func main() {
   service := new(ServiceA)
   rpc.RegisterName("AddSrv", service)
   l, err := net.Listen("tcp", ":8081")
   if err != nil {
      log.Fatalln(err)
   }
   rpc.Accept(l)
}

go 语言的 rpc 包使用 gob 协议对传输数据进行序列化和反序列化,所以我们注册的服务有一点要求便是参数必须能被 encoding/gob 序列化。除此之外还有以下要求:

  • 方法的类型是可导出的(公开的);
  • 方法本身也是可导出的;
  • 方法必须有 2 个参数,并且参数类型是可导出或者内建的;
  • 方法必须返回一个 error 类型。
func (s *ServiceType)MethodName(arg argType,resp *respType)error

还有两点需要注意:

  • 第一个参数 argType 是调用者(客户端)提供的;
  • 第二个参数 replyType是返回给调用者结果,必须是指针类型。

在上面我们已经解决了服务端的问题,接下来我们演示客户端如何调用这一方法。

func call() {
   time.Sleep(time.Second) // 先睡一秒保证服务端的服务已经启动
   client, err := rpc.Dial("tcp", "localhost:8081")
   if err != nil {
      log.Println(err)
      return
   }
   args := &Args{A: 10, B: 20}
   resp := new(int)
   err = client.Call("MathSrv.Add", args, resp)
   if err != nil {
      log.Println("call Add service failed err:", err)
      return
   }
   log.Printf("%d + %d = %d\n", args.A, args.B, *resp)
}

在以上实例代码中,首先通过 rpc.Dial 函数建立 TCP 链接,需要注意的是这里的 IP、端口要和 RPC 服务提供的一致,确保可以建立 RCP 链接。

TCP 链接建立成功后,就需要准备远程方法需要的参数,也就是示例中的args 和 resp。参数准备好之后,就可以通过 Call 方法调用远程的RPC 服务了。Call 方法有 3 个参数,它们的作用分别如下所示:

  1. 调用的远程方法的名字,这里是MathSrv.Add,点前面的部分是注册的服务的名称,点后面的部分是该服务的方法
  2. 客户端为了调用远程方法提供的参数,示例中是 args;
  3. 为了接收远程方法返回的结果,必须是一个指针,也就是示例中的 &resp ,这样客户端就可以获得服务端返回的结果了。

然后我们在 rpc.Accept 前开一个协程来跑客户端代码。

得到以下输出

2023/05/29 21:25:22 10 + 20 = 30

这代表我们调用成功了

go 语言为我们内置的 rpc 实现还有许多,如基于 HTTP 的 rpc,还有使用 JSON 序列化的 rpc 实现,上面只演示了一种,其他的大家可以自行了解。