用一个简单的函数来理一下RPC调用过程

1.什么是RPC

RPC(Remote Procedure Call)远程过程调度,简单的理解就是一个节点请求另一个节点的提供的服务。

2.远程调用要面临的三个问题。

(1)Call ID映射。本地调用中,函数体是直接通过函数指针来指定的,调用函数时,编译器会自动调用它相应的函数指针。但在远程调用中,函数指针是不行的,因为两个进程的地址空间完全不一样。所以,在RPC中所有的函数都必须有自己的一个ID。这个ID在所有进程中都是唯一确定的。客户端在做远程过程调用时,必须附上这个ID。然后我们还需要在客户端和服务端分别维护一个 {函数 :Call ID} 的对应表。两者的表不一定需要完全相同,但相同的函数对应的Call ID必须相同。当客户端需要进行远程调用时,它就查该表,找出相应的Call ID,然后把它传给服务端,服务端也通过查表,来确定客户端需要调用的函数,然后执行相应函数的代码。

(2)序列化和反序列化。客户端如何将参数值传给远程的函数就成了一个问题在本地调用中,只需要把参数压到栈里,函数自己去栈里读即可。在远程过程调用时,客户端跟服务端是不同的进程,不能通过内存来传递参数。甚至有时候客户端和服务端使用的都不是同一种语言(比如服务端用C++,客户端用Java或者Python)。这时需要客户端把参数先转成一个字节流,传给服务端后,再把字节流转成自己能读取的格式。这个过程叫序列化和反序列化(也叫编码和解码)。同理,从服务端返回的值也需要序列化反序列化的过程。

(3)网络传输。远程调用往往用在网络上,客户端和服务端是通过网络连接的。所有的数据都需要通过网络传输,因此就需要有一个网络传输层。网络传输层需要把Call ID和序列化后的参数字节流传给服务端,然后再把序列化后的调用结果传回客户端。只要能完成这两者的,都可以作为传输层使用。因此,它所使用的协议其实是不限的,能完成传输就行。尽管大部分RPC框架都使用TCP协议,但其实UDP也可以,而gRPC干脆就用了HTTP2,Java的Netty也属于这层的东西。

3.RPC调用过程是怎么样的

PRC的过程大概就是客户端让服务端做一件事,服务端将处理结果发给客户端。笔者认为可以用这样的一个例子来比喻:

客户端好比一个“老板”服务端好比一个“仓库”“老板”要从“仓库”要东西,“仓库”会自动将“老板”要的东西下单发送给“老板”,但是这样的过程是不是有点不太好管理,因为“老板”必须将注意力放到重大决策上,不能过多关注于如何去联系“仓库”,所以,“老板”需要有一个“秘书”来帮他处理这些事情,“仓库”也一样,每天有那么多“老板”都要东西,需要有一个“仓库管理员”来帮接收“老板”要什么东西,将东西打包发给“老板”

老板:

  • 客户端(client):服务调用发起方,也称为消费者。

秘书:

  • 客户端存根(client Stub):该程序运行在客户端所在的计算机机器上,主要用来存储要调用的服务器的地址,另外,该程序还负责将客户端请求远端服务器程序的数据信息打包成数据包,通过网络发送给服务端Stub程序;其次,还要接收服务端Stub程序发送的调用结果数据包,并解析返回给客户端。

仓库:

  • 服务端(server):远端的计算机机器上运行的程序,其中有客户端要调用的方法。

仓库管理员:

  • 服务端存根(server Stub):接收客户Stub程序通过网络发送的请求消息数据包,并调用服务端中真正的程序功能方法,完成功能调用;其次,将服务端执行调用的结果进行数据处理打包发送给客户端Stub程序。

 RPC调用过程为:

  • 客户端想要发起一个远程过程调用,首先通过调用本地客户端Stub程序的方式调用想要使用的功能方法名;
  • 客户端Stub程序接收到了客户端的功能调用请求,将客户端请求调用的方法名,携带的参数等信息做序列化操作,并打包成数据包。
  • 客户端Stub查找到远程服务器程序的IP地址,调用Socket通信协议,通过网络发送给服务端。
  • 服务端Stub程序接收到客户端发送的数据包信息,并通过约定好的协议将数据进行反序列化,得到请求的方法名和请求参数等信息。
  • 服务端Stub程序准备相关数据,调用本地Server对应的功能方法进行,并传入相应的参数,进行业务处理。
  • 服务端程序根据已有业务逻辑执行调用过程,待业务执行结束,将执行结果返回给服务端Stub程序。
  • 服务端Stub程序将程序调用结果按照约定的协议进行序列化,并通过网络发送回客户端Stub程序。
  • 客户端Stub程序接收到服务端Stub发送的返回数据,对数据进行反序列化操作,并将调用返回的数据传递给客户端请求发起者。
  • 客户端请求发起者得到调用结果,整个RPC调用过程结束。

4.用一个简单的函数来看看这个过程是怎么样的

先看下目录结构:

handler(放处理函数)

package handler

const HelloServiceName = "handler/HelloService"

type NewHelloService struct {
}

func (s *NewHelloService) Hello(request string, reply *string) error {
	//返回值是通过修改reply
	*reply = "hello," + request
	return nil
}

服务端(仓库):server.go

package main

import (
	"microServices/rpc_call/handler"
	"microServices/rpc_call/server_proxy"
	"net"
	"net/rpc"
)

func main() {
	//1.实例化一个server
	listener, _ := net.Listen("tcp", ":12345")

	//2.注册处理逻辑  handler
	_ = server_proxy.RegisterHelloService(&handler.NewHelloService{})
	//3.启动服务
	for {
		conn, _ := listener.Accept()
		go rpc.ServeConn(conn)
	}

}

服务端存根(仓库管理员):server_proxy.go

package server_proxy

import (
	"microServices/rpc_call/handler"
	"net/rpc"
)

type HelloServicer interface {
	Hello(request string, reply *string) error
}

func RegisterHelloService(srv HelloServicer) error {
	return rpc.RegisterName(handler.HelloServiceName, srv)
}
func main() {

}

客户端(老板):client.go

package main

import (
	"fmt"
	"microServices/rpc_call/client_proxy"
)

func main() {
	//1.建立连接
	client := client_proxy.NewHelloServiceStub("tcp", "localhost:12345")
	//1.只写业务逻辑,不关注每个函数的名称
	var reply string
	err := client.Hello("voyager", &reply)
	if err != nil {
		panic("调用失败")
	}
	fmt.Println(reply)
}

客户端存根(秘书):client_proxy.go

package client_proxy

import (
	"microServices/rpc_call/handler"
	"net/rpc"
)

type HelloServiceStub struct {
	*rpc.Client
}

func NewHelloServiceStub(protol, address string) HelloServiceStub {
	conn, err := rpc.Dial(protol, address)
	if err != nil {
		panic("connect error!")
	}
	return HelloServiceStub{conn}
}

func (c *HelloServiceStub) Hello(request string, reply *string) error {
	err := c.Call(handler.HelloServiceName+".Hello", request, reply)
	if err != nil {
		return err
	}

	return nil
}

梳理一下调用流程:

  • client端通过调用了本地的client_proxy的NewHelloServiceStub函数建立了连接
  • 然后client端要调用Hello这个函数。
  • client_proxy将hello的方法名,携带的参数做序列化操作,并打包成数据包,发出请求调用hello这个函数。
  • handler里面的函数实现了server_proxy里面的接口,所以在server端在注册处理逻辑的时候可以传入handler里面的函数。
  • server处理完返回给client端

为什么server_proxy里面要用interface?

因为如果不用的话,server_proxy和handler的耦合性太强, 也就是说handler里面的结构体改了,server_proxy也必须改,而server_proxy的RegisterHelloService方法关注的是方法是否被实现,在go里面interface刚好可以解决这个问题。

  • 0
    点赞
  • 2
    收藏
    觉得还不错? 一键收藏
  • 0
    评论

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值