一. 什么是RPC
- 参考博客: Go RPC 开发指南
- 什么是RPC: 全称Remote Procedure Call, 即远程过程调用,主要作用是,实现像调用本地方法一样调用远程方法,并且屏蔽底层网络编程细节
1. RPC 实现原理
- 首先整个RPC架构中可以分为以下几个模块
- 客户端服务调用方Client
- 客户端存根Client Stub: 存放服务端地址信息,将客户端的请求信息打包成网络消息,再通过网络传输发送给服务端
- 服务端存根Server Stub: 接收客户端发送过来的请求消息并进行解包,然后调用本地服务进行处理
- 服务端Server: 服务的真正提供者
- RPC调用过程: 启动服务提供方–>暴露服务–> 服务消费方启动–>服务发现–>服务引用–>服务消费方调用服务提供方暴露的方法
2. 有http为什么还要出现RPC
- 或者问RPC与http有什么区别
- 首先在服务注册,发现层面,没有什么区别,比如SpringCloud中基于http通信,基于Nacos,或Eureka等等作为注册中心,进行服务注册,服务发现,发起调用的
- 在细节上
- HTTP是一种超文本传输协议,默认情况下基于JSON格式传输数据,跨语言, 简单易用,但是效率低
- RPC 远程过程调用,是一种计算机通信协议,基于二进制进行数据传输,在发送与接收数据时根据指定协议进行序列化,反序列化,性能略高,但是因为序列化问题就造成了没有HTTP灵活,比如不能跨语言
- HTTP与RPC底层都基于socket通信,但是HTTP支持资源定位的路径支持RESTful风格,RPC不支持
- 参考博客
3. Protobut
- 参考博客
- 前面说过,在使用RPC时可以指定序列化协议,反序列化协议,在发送数据与接收数据时根据直接协议进行序列化反序列化
- 常见的序列化反序列化协议有哪些:
- json 序列化
- 比如使用Dubbo时hessian2序列化
- Google中提出的Protobuf等等
- 讲一下Protobuf序列化原理, 首先说一下Protobuf使用流程
- 定义 .proto 文件
- 使用protoc 编译器编译 .proto 文件生成一系列接口代码
- 调用生成的接口实现对 .proto 定义的字段的读取以及 message 对象的序列化、反序列化方法
Protobuf 编码方式
- Protobuf支持两种编码方式:
- Varint编码: 一种变长的编码方式,编码原理是用字节表示数字,值越小的数字,使用越少的字节数表示。因此,可以通过减少表示数字的字节数进行数据压缩
- Zigzag编码: 一种变长的编码方式,编码原理是使用无符号数来表示有符号数字,使得绝对值小的数字都可以采用较少字节来表示,特别对表示负数的数据能更好地进行数据压缩
Protobuf 数据存储方式
- 支持两种
- T-L-V数据存储方式: 即标识符-长度-字段值的存储方式,其原理是以标识符-长度-字段值表示单个数据,最终将所有数据拼接成一个字节流,从而实现数据存储的功能。其中Length可选存储,如储存Varint编码数据就不需要存储Length,此时为T-V存储方式。
- T-V数据存储方式: 消息字段的标识号、数据类型、字段值经过Protobuf采用Varint和Zigzag编码后,以T-V(Tag-Value)方式进行数据存储。对于Varint与Zigzag编码方式编码的数据,省略了T-L-V中的字节长度Length
Protobuf对于数据存储的三大原则
- Protocol Buffer将消息中的每个字段进行编码后,利用T - L - V 存储方式进行数据的存储,最终得到一个二进制字节流。
- Protobuf对于不同的字段类型采用不同的编码和数据存储方式对消息字段进行序列化,以确保得到高效紧凑的数据压缩对于Varint编码数据的存储,不需要存储字节长度Length,使用T-V存储方式进行存储;对于采用其它编码方式(如LENGTH_DELIMITED)的数据,使用T-L-V存储方式进行存储。
- ProtoBuf对于数据字段值的独特编码方式与T-L-V数据存储方式,使得 ProtoBuf序列化后数据量体积极小。
Protobuf 序列化原理
- Protobuf 序列化: Protobuf对于不同的字段类型采用不同的编码方式和数据存储方式进行序列化,确保高效紧凑,序列化过程
- 判断每个字段是否有设置值,有值才进行编码。
- 根据字段标识号与数据类型将字段值通过不同的编码方式进行编码。
- 将编码后的数据块按照字段类型采用不同的数据存储方式封装成二进制数据流。
- Protobuf 反序列化过程如下:
- 调用消息类的parseFrom(input)解析从输入流读入的二进制字节数据流。
- 将解析出来的数据按照指定的格式读取到Java、C++、Phyton对应的结构类型中
4. 其它问题
- protobuf的序列化流程: 参考序列化原理内部的序列化部分, 然后讲一下protobuf的编码方式与存储方式,另外还有存储原则
- go的struct通过grpc传输的时候,是一个二进制的字节流,struct变成字节流的过程?是怎么拼成字节流的?跟上面一样
- 用rpc请求和http请求的区别或gRPC和REST的区别是什么,和rpc的优势是什么,是什么造成rpc的性能更好: 参考"2. 有http为什么还要出现RPC"
- RPC接口和RESTful接口是怎么做的: 在grpc框架中提供了grpc-gateway,如果rpc接口需要支持RESTful,通过grpc-gateway实现
- Protocol Buffers是什么,为什么它被用于gRPC中: 一种高效的序列化,反序列化协议
- gRPC的流程是什么: 定义服务、生成源代码、实现服务、启动服务。首先,需要定义要实现的服务及其接口,使用Protocol Buffers编写接口定义文件。其次,使用编译器生成客户端和服务器端的源代码。然后,实现生成的接口。最后,启动服务器并将其部署在适当的位置
- gRPC支持哪些类型的序列化: :二进制(使用Protocol Buffers)和JSON。其中,二进制序列化在效率和性能方面比JSON序列化更优秀。但是,JSON序列化在可读性方面更好,可以方便地进行调试和测试
- grpc底层通信协议: gRPC底层使用的HTTP/2协议, HTTP协议本身可以通过Content-Encoding表示压缩算法,使用Contetn-length指定数据长度
-
二. golang 标准库与 RPC
1. net/rpc库
- golang标准库"net/rpc"提供了RPC支持
- 使用http作为RPC的载体,通过net/http包监听客户端连接请求, 如下监听本地的8095端口
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:8095")
if err != nil {
log.Fatalln("fatal error: ", err)
}
fmt.Fprintf(os.Stdout, "%s", "start connection")
http.Serve(lis, nil)
}
- rpc_client 客户端示例
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:8095")
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)
}
2. net/rpc/jsonrpc库
- 上面存在问题: 上面的例子演示了使用net/rpc实现RPC的过程,但是没办法在其他语言中调用上面例子实现的RPC方法。所以接下来的例子演示使用net/rpc/jsonrpc库实现RPC方法,此方式实现的RPC方法支持跨语言调用,也就是net/rpc/jsonrpc库
- 提供一个服务端,监听本地的8096端口,并处理客户端的tcp连接请求
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:8096")
if err != nil {
log.Fatalln("fatal error: ", err)
}
fmt.Fprintf(os.Stdout, "%s", "start connection")
for {
conn, err := lis.Accept() // 接收客户端连接请求
if err != nil {
continue
}
go func(conn net.Conn) { // 并发处理客户端请求
fmt.Fprintf(os.Stdout, "%s", "new client in coming\n")
jsonrpc.ServeConn(conn)
}(conn)
}
}
- 提供一个客户端
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:8096")
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)
}