首页
什么是 RPC 协议

Q:什么是 RPC 简介?

  • 远程过程调用(Remote Procedure Call,RPC)是一个计算机通信协议

  • 该协议允许运行于一台计算机的程序调用另一台计算机的子程序,而程序员无需额外地为这个交互作用编程

  • 如果涉及的软件采用面向对象编程,那么远程过程调用亦可称作远程调用或远程方法调用

Q:RPC 协议的作用是什么?

RPC 的作用就是体现在这样两个方面:

  • 屏蔽远程调用跟本地调用的区别,让我们感觉就是调用项目内的方法;

  • 隐藏底层网络通信的复杂性,让我们更专注于业务逻辑。

Q:RPC 通信流程是什么?

  • RPC 是一个远程调用,所以要通过网络来传输数据,并且 RPC 常用于业务系统之间的数据交互,需要保证其可靠性,所以 RPC 协议一般默认采用 TCP 来传输

  • 网络传输的数据必须是二进制数据,但调用方请求的出入参数都是对象。对象是肯定没办法直接在网络中传输的,需要提前把它转化成可以传输的二进制,并且要求转换算法是可逆的,这个过程我们一般叫做”序列化

  • 根据协议格式,服务提供方就可以正确地从二进制数据中分割出不同的请求来,同时根据请求类型和序列化类型,把二进制的消息体逆向还原成请求对象。这个过程叫作“反序列化”。

  • 服务提供方再根据反序列化出来的请求对象找到对应的实现类,完成真正的方法调用,然后把执行结果序列化后,回写到对应的 TCP 通道里面。调用方获取到应答的数据包后,再反序列化成应答对象,这样调用方就完成了一次 RPC 调用。

RPC 的通信流程通常包括以下步骤:

  1. 客户端调用:客户端调用远程服务的接口或方法,就像调用本地函数一样。客户端需要提供远程调用的参数,并等待服务器的响应。

  2. 参数序列化:客户端将调用参数序列化为可传输的格式,通常是二进制数据或者类似于 JSON、XML 等的文本格式。序列化的过程将参数转换成一个可以在网络上传输的形式。

  3. 网络传输:客户端通过网络将序列化后的参数发送给远程服务端。这通常涉及到底层的网络通信,例如 TCP 或者 HTTP 等协议。

  4. 服务端反序列化:服务端接收到客户端发送的参数后,需要进行反序列化,将接收到的数据解析成可识别的格式,恢复成原始的调用参数。

  5. 服务端调用:服务端根据接收到的参数调用相应的函数或方法,并执行相应的业务逻辑。服务端处理完请求后,会得到一个结果。

  6. 结果序列化:服务端将执行结果序列化为可传输的格式,与调用参数类似,通常是二进制数据或者特定的文本格式。

  7. 网络传输:服务端将序列化后的执行结果通过网络发送给客户端。

  8. 客户端反序列化:客户端接收到服务端发送的执行结果后,需要进行反序列化,将接收到的数据解析成可识别的格式,恢复成原始的执行结果。

  9. 客户端处理结果:客户端根据接收到的执行结果进行相应的处理,例如将结果显示给用户,或者进行下一步的业务逻辑处理。

总的来说,RPC 协议的通信流程包括参数序列化、网络传输、参数反序列化、远程调用、结果序列化、网络传输、结果反序列化等步骤,通过这些步骤实现客户端和服务端之间的远程调用和数据传输。

Q:RPC 框架是如何屏蔽底层细节的?

对于研发人员来说,上面的过程要掌握太多的 RPC 底层细节,需要手动写代码去构造请求、调用序列化,并进行网络调用,整个 API 非常不友好。

那我们有什么办法来简化 API,屏蔽掉 RPC 细节,让使用方只需要关注业务接口,像调用本地一样来调用远程呢?

如果你了解 Spring,一定对其AOP技术很佩服,其核心是采用动态代理的技术,通过字节码增强对方法进行拦截增强,以便于增加需要的额外处理逻辑。其实这个技术也可以应用到 RPC 场景来解决我们刚才面临的问题。

由服务提供者给出业务接口声明,在调用方的程序里面,RPC 框架根据调用的服务接口提前生成动态代理实现类,并通过依赖注入等技术注入到声明了该接口的相关业务逻辑里面。该代理实现类会拦截所有的方法调用,在提供的方法处理逻辑里面完成一整套的远程调用,并把远程调用结果返回给调用方,这样调用方在调用远程方法的时候就获得了像调用本地接口一样的体验。

到这里,一个简单版本的 RPC 框架就实现了。我把整个流程都画出来了,供你参考:

image.png

Q:PRC 框架有哪些功能?

我认为,一个完整的 RPC 框架,应包含负载均衡、服务注册和发现、服务治理等功能,并具有可拓展性便于流量监控系统等接入那么它才算完整的,当然了。有些较单一的 RPC 框架,通过组合多组件也能达到这个标准。

image.png

Q:golang 如何实现 RPC?

  • golang 中实现 RPC 非常简单,官方提供了封装好的库,还有一些第三方的库

  • golang 官方的 net/rpc 库使用 encoding/gob 进行编解码,支持 tcp 和 http 数据传输方式,由于其他语言不支持 gob 编解码方式,所以 golang 的 RPC 只支持 golang 开发的服务器与客户端之间的交互

  • 官方还提供了net/rpc/jsonrpc 库实现 RPC 方法,jsonrpc 采用 JSON 进行数据编解码,因而支持跨语言调用,目前 jsonrpc 库是基于 tcp 协议实现的,暂不支持 http 传输方式

  • 示例代码:使用 rpc

package main


import (
"log"
"net/http"
"net/rpc"
)

type Params struct {
Width, Height int
}

type Rect struct{}

// RPC 服务方法,求矩形面积
func (r *Rect) Area(p Params, ret *int) error {
_ret = p.Height _ p.Width
return nil
}

// RPC 服务方法,求矩形周长
func (r *Rect) Perimeter(p Params, ret *int) error {
_ret = (p.Height + p.Width) _ 2
return nil
}

func main() {
rect := new(Rect)
// 1. 注册一个 rect 的服务
rpc.Register(rect)
// 2. 将服务绑定到 http 协议上
rpc.HandleHTTP()
// 3. 监听服务
err := http.ListenAndServe(":8000", nil)
if err != nil {
log.Panicln(err)
}
}

package main

import (
	"fmt"
	"log"
	"net/rpc"
)

type Params struct {
	Width, Height int
}

func main() {
	//1. 连接远程rpc服务器
	conn, err := rpc.DialHTTP("tcp", ":8000")
	if err != nil {
		log.Fatal(err)
	}
	// 2. 调用方法
	// 求面积
	ret := 0
	err = conn.Call("Rect.Area", Params{5, 10}, &ret)
	if err != nil {
		log.Fatal(err)
	}

	fmt.Println("面积:", ret)

	err = conn.Call("Rect.Perimeter", Params{5, 10}, &ret)
	if err != nil {
		log.Fatal(err)
	}
	fmt.Println("周长:", ret)
}
  • golang 写 RPC 程序,必须符合 4 个基本条件,不然 RPC 用不了

    • 结构体字段首字母要大写,可以别人调用

    • 函数名必须首字母大写

    • 函数第一参数是接收参数,第二个参数是返回给客户端的参数,必须是指针类型

    • 函数还必须有一个返回值 error

  • 示例代码:使用 jsonprc

package main


import (
"fmt"
"log"
"net"
"net/rpc"
"net/rpc/jsonrpc"
)

type Params struct {
Width, Height int
}

type Rect struct{}

// RPC 服务方法,求矩形面积
func (r *Rect) Area(p Params, ret *int) error {
_ret = p.Height _ p.Width
return nil
}

// RPC 服务方法,求矩形周长
func (r *Rect) Perimeter(p Params, ret *int) error {
_ret = (p.Height + p.Width) _ 2
return nil
}

func main() {
rectServer := new(Rect)
// 1. 注册一个 rect 的服务
rpc.Register(rectServer)

    // 2. 监听端口
    listener, err := net.Listen("tcp", ":8000")
    if err != nil {
    	log.Panicln(err)
    }

    // 3. 开始建立连接
    for {
    	fmt.Println("开始建立连接...")
    	conn, err := listener.Accept()
    	if err != nil {
    		fmt.Println(err)
    	}
    	rpc.ServeCodec(jsonrpc.NewServerCodec(conn))
    }

}

package main

import (
	"fmt"
	"log"
	"net"
	"net/rpc"
	"net/rpc/jsonrpc"
)

type Params struct {
	Width, Height int
}

func main() {
	//1. 使用 net.Dial 和 rpc 微服务建立连接
	conn, err := net.Dial("tcp", ":8000")
	if err != nil {
		log.Fatal(err)
	}
	defer conn.Close()
	// 2. 建立基于json编码的rpc服务
	client := rpc.NewClientWithCodec(jsonrpc.NewClientCodec(conn))

	// 3. 调用远程函数
	// 求面积
	ret := 0
	err = client.Call("Rect.Area", Params{5, 10}, &ret)
	if err != nil {
		log.Fatal(err)
	}

	fmt.Println("面积:", ret)

	err = client.Call("Rect.Perimeter", Params{5, 10}, &ret)
	if err != nil {
		log.Fatal(err)
	}
	fmt.Println("周长:", ret)
}

Q:微服务下 PRC 调用流程是什么?

  • 微服务架构下数据交互一般是对内 RPC,对外 REST

  • 将业务按功能模块拆分到各个微服务,具有提高项目协作效率、降低模块耦合度、提高系统可用性等优点,但是开发门槛比较高,比如 RPC 框架的使用、后期的服务监控等工作

  • 一般情况下,我们会将功能代码在本地直接调用,微服务架构下,我们需要将这个函数作为单独的服务运行,客户端通过网络调用