RPC协议

微服务架构

将大型应用程序拆分为多个小型、独立服务的架构模式。其核心特点包括:

  • 服务拆分:将应用程序分解为多个小型服务,每个服务负责特定的业务功能 独立部署:每个服务可以独立开发、测试和部署,互不影响
  • 轻量级通信:服务之间通过轻量级的通信机制(如HTTP/REST、gRPC等)进行交互,降低耦合度
  • 技术多样性:不同服务可以使用不同的编程语言和技术栈,以适应不同的业务需求
  • 高可用性:服务可以水平扩展,以应对高流量和高并发请求

造成问题:

  1. 代码冗余
  2. 服务和服务之间存在调用关系
    服务拆分后是,服务和服务之间、进程和进程之间的调用
    需要发起网络调用,使用http虽然便捷,但是在微服务中性能较低

RPC

一种进程间通信协议,允许程序调用另一台机器上的函数/过程
引入RPC(远程过程调用),通过自定义协议发起TCP调用,加快传输效率
RPC屏蔽分布式计算中的各种调用细节,可以像本地调用一样直接调用一个远程函数
底层通过序列化(编码)请求、网络传输、远程解码执行,再将结果返回

客户端与服务端沟通的过程:
客户端发送数据(字节流的形式)
服务端接收并解析,根据约定执行指定操作,把结果返回给客户端
RPC就是将上述过程进行封装,使操作更加优化
使用广泛认可的协议,使其规范化

RPC 的基本流程:

  1. 客户端(Client)通过本地调用的方式调用服务(以接口方式调用)
  2. 客户端存根(Client Stub)接收到调用请求后负责将方法、入参等信息进行组装序列化成能够进行网络传输的消息体(将消息体对象序列化为二进制流)
  3. 客户端存根(Client Stub)找到远程的服务地址,并且将消息通过网络发送给服务端(通过sockets发送消息)
  4. 服务端存根(Server Stub)收到消息后进行反序列化操作,即解码(将二进制流反序列化为消息对象)
  5. 服务端存根(Server Stub)通过解码结果调用本地的服务进行相关处理
  6. 服务端(Server)本地服务业务处理
  7. 服务端(Server)将处理结果返回给服务端存根
  8. 服务端存根(Server Stub)序列化处理结果(将结果消息对象序列化为二进制流)
  9. 服务端存根(Server Stub)将序列化结果通过网络发送至客户端(通过sockets发送消息)
  10. 客户端存根(Server Stub)接收到消息,进行反序列化解码(将结果二进制流反序列化为消息对象)
  11. 客户端得到最终的结果。

在这里插入图片描述

gRPC

高性能,开源的通用RPC框架,基于“服务定义”的思想
调用方:client
被调用方:server
通过某种方式描述服务,描述于语言无关,在”服务定义“的过程中,描述提供的服务服务名,可被调用的方法,方法的入参和回参
在定义好服务和方法后,gRPC屏蔽底层细节,client直接调用定义好的方法,就可以拿到预期的返回结果,在server端需要实现定义的方法,只需要实现定义方法的具体逻辑
双方约定好接口,server实现接口,client调用这个接口的代理对象
可以使用C++作为服务端,Golang,Java作为客户端,所以在”定义服务“和编码解码的过程中,应该与语言无关
在这里插入图片描述

Protocol Buffss

使用Protocol Buffss(一套成熟的数据结构序列化机制)将定义的方法,转换为特定语言的代码
例如:一种类型的参数,会帮忙转为Golang中的struct结构体,定义的方法转为func函数,在发送和接收请求的时候,会完成编码和解码的工作
将即将发送的数据编码为gRPC能够传输的形式,将接收到的数据解码为编程语言可以理解的数据格式
序列化:数据结构或对象转为二进制串的过程
反序列化:二进制串转为数据结构或对象的过程

protobuf

一种数据格式,适合高性能,对响应速度有要求的数据传输场景
二进制数据格式,需要编码和解码,本身不具有可读性

  1. 序列化后体积相比json,xml很小,适合网络传输
  2. 支持跨平台多语言
    Protobuf 使用一种简单、语言无关的 .proto 文件来描述数据结构。这种文件就像是一个通用的 “模板”,它定义了消息(message)的结构,包括消息中包含的字段以及每个字段的类型(如 int32、string 等)和标签号(tag)
    只需要根据这个 .proto 文件定义来生成对应语言的代码
  3. 消息格式升级和兼容性不错
  4. 序列化反序列化速度很快

安装protobuf

下载并解压
配置环境变量
在这里插入图片描述
cmd窗口运行protoc命令
在这里插入图片描述
安装生成对应语言的代码,生成对应的业务代码

go get google.golang.org/grpc

执行安装工具

go install google.golang.org/grpc/cmd/protoc-gen-go-grpc@latest
go install google.golang.org/protobuf/cmd/protoc-gen-go@lastest 

编写.proto文件

// 使用的语法
syntax = "proto3";

// 生成go文件的目录当前目录生成的go文件包名为service
option go_package = ".;service";

// 定义服务,在服务中有一个方法,可以接收客户端的参数,返回服务端的响应
service SayHello {
  rpc SayHello (HelloRequest) returns (HelloResponse) {}
}

// message 关键字,类似于结构体
// 定义变量在message中的位置
message HelloRequest {
  string requestName = 1;
//  int64 age = 2;
}

message HelloResponse {
  string responseMsg = 1;
}

进入到.proto文件所在目录
执行命令

protoc --go_out=. hello.proto

生成hello.pb.go文件
在这里插入图片描述
执行命令

protoc --go-grpc_out=. hello.proto

生成hello_grpc.pb.go文件
在这里插入图片描述
需要指定业务逻辑时,重写该方法

func (c *sayHelloClient) SayHello(ctx context.Context, in *HelloRequest, opts ...grpc.CallOption) (*HelloResponse, error) {
	cOpts := append([]grpc.CallOption{grpc.StaticMethod()}, opts...)
	out := new(HelloResponse)
	err := c.cc.Invoke(ctx, SayHello_SayHello_FullMethodName, in, out, cOpts...)
	if err != nil {
		return nil, err
	}
	return out, nil
}

RequestName 放在请求参数中的第一个位置

type HelloRequest struct {
	state         protoimpl.MessageState `protogen:"open.v1"`
	RequestName   string                 `protobuf:"bytes,1,opt,name=requestName,proto3" json:"requestName,omitempty"` //  int64 age = 2;
	unknownFields protoimpl.UnknownFields
	sizeCache     protoimpl.SizeCache
}

proto文件

一个约束

message

protobuf中定义一个消息类型式是通过关键字message字段指定的
消息就是需要传输的数据格式的定义,类似于结构体
在消息中承载的数据分别对应每个字段,其中每个字段有一个名字和类型
一个文件中可以定义多个消息类型

规则

required:消息体中必填字段,不设置会导致编码异常,在protobuf2中使用
optional:消息体中可选字段,protobuf3中没有required和optional等说明关键字,都默认为optional
repeated:消息体中可重复字段,重复的值的顺序会被保留在go中被定义为切片
消息体中每个字段的标识号是唯一的 [1,2^29-1]的整数

message HelloRequest {
  string requestName = 1;
  int64 age = 2;
  repeated string tags = 3;
}
type HelloRequest struct {
	state         protoimpl.MessageState `protogen:"open.v1"`
	RequestName   string                 `protobuf:"bytes,1,opt,name=requestName,proto3" json:"requestName,omitempty"`
	Age           int64                  `protobuf:"varint,2,opt,name=age,proto3" json:"age,omitempty"`
	Tags          []string               `protobuf:"bytes,3,rep,name=tags,proto3" json:"tags,omitempty"`
	unknownFields protoimpl.UnknownFields
	sizeCache     protoimpl.SizeCache
}

嵌套消息

message PersonInfo{
  message Person{
    string name = 1;
    int32 height = 3;
    repeated int32 weight = 2;
  }
  repeated Person info = 1;
}

message PersonMessage {
  PersonInfo.Person info = 1;
}
type PersonInfo struct {
	state         protoimpl.MessageState `protogen:"open.v1"`
	Info          []*PersonInfo_Person   `protobuf:"bytes,1,rep,name=info,proto3" json:"info,omitempty"`
	unknownFields protoimpl.UnknownFields
	sizeCache     protoimpl.SizeCache
}
type PersonMessage struct {
	state         protoimpl.MessageState `protogen:"open.v1"`
	Info          *PersonInfo_Person     `protobuf:"bytes,1,opt,name=info,proto3" json:"info,omitempty"`
	unknownFields protoimpl.UnknownFields
	sizeCache     protoimpl.SizeCache
}
type PersonInfo_Person struct {
	state         protoimpl.MessageState `protogen:"open.v1"`
	Name          string                 `protobuf:"bytes,1,opt,name=name,proto3" json:"name,omitempty"`
	Height        int32                  `protobuf:"varint,3,opt,name=height,proto3" json:"height,omitempty"`
	Weight        []int32                `protobuf:"varint,2,rep,packed,name=weight,proto3" json:"weight,omitempty"`
	unknownFields protoimpl.UnknownFields
	sizeCache     protoimpl.SizeCache
}

服务定义

将消息类型用在RPC系统中,要在.proto文件中定义一个RPC服务端口,protocol buffer将会根据选择不同语言生成服务接口代码和存根

service SearchService {
// rpc 服务函数名 (参数) 返回 (返回参数)
  rpc Search (SearchRequest) returns (SearchResponse) {}
}

服务端编写

实现约束

  1. 创建gRPC Server 对象,Server端的抽象对象
  2. 将server(需要被调用的服务端接口) 注册到gRPC Server的内部注册中心
    在接收到请求时,通过内部的服务发现,该服务端接口并转接进行逻辑处理
  3. 创建Listen,监听TCP端口
  4. gRPC Server开始lis.Accept ,直到stop
package main

import (
	"context"
	"google.golang.org/grpc"
	pb "grpc/server/proto"
	"net"
)

// grpc中定义的结构体
// type UnimplementedSayHelloServer struct{}
type server struct {
	// 继承文件中的方法
	pb.UnimplementedSayHelloServer
}

// SayHello 重写方法
func (s *server) SayHello(ctx context.Context, req *pb.HelloRequest) (*pb.HelloResponse, error) {
	return &pb.HelloResponse{
		ResponseMsg: "hello " + req.RequestName,
	}, nil
}

func main() {
	// 开启端口监听
	listen, _ := net.Listen("tcp", ":8080")
	// 创建grpc服务
	grpcServer := grpc.NewServer()
	// 注册服务
	pb.RegisterSayHelloServer(grpcServer, &server{})
	// 启动服务
	err := grpcServer.Serve(listen)
	if err != nil {
		panic(err)
	}
}

/*
func RegisterSayHelloServer(s grpc.ServiceRegistrar, srv SayHelloServer) {
	// If the following call pancis, it indicates UnimplementedSayHelloServer was
	// embedded by pointer and is nil.  This will cause panics if an
	// unimplemented method is ever invoked, so we test this at initialization
	// time to prevent it from happening at runtime later due to I/O.
	if t, ok := srv.(interface{ testEmbeddedByValue() }); ok {
		t.testEmbeddedByValue()
	}
	s.RegisterService(&SayHello_ServiceDesc, srv)
}
*/

客户端编写

调用

  1. 创建与给定服务端的连接交互
  2. 创建server的客户端对象
  3. 发送RPC请求,等待同步响应,得到回调后返回响应结果
  4. 输出响应结果
package main

import (
	"context"
	"fmt"
	"google.golang.org/grpc"
	"google.golang.org/grpc/credentials/insecure"
	pb "grpc/server/proto"
)

func main() {
	// 创建grpc连接,不进行加密验证传输
	conn, _ := grpc.NewClient(":8080", grpc.WithTransportCredentials(insecure.NewCredentials()))
	defer func(conn *grpc.ClientConn) {
		err := conn.Close()
		if err != nil {

		}
	}(conn)

	// 建立连接
	client := pb.NewSayHelloClient(conn)

	// 执行rpc调用(在服务器实现并返回结果)
	resp, _ := client.SayHello(context.Background(), &pb.HelloRequest{RequestName: "yf"})
	fmt.Println(resp.GetResponseMsg())
}

认证-安全传输

gRPC是典型的C/S模型,开发客户端和服务端并达成协议,使用确认的传输协议传输数据
默认使用protobuf作为传输协议,也可以使用其他自定义的
在这里插入图片描述
客户端和服务端通信,要明确要发送的服务端和要返回的客户端
gRPC认证,不是用户身份认证,而是指多个server和多个client之间,进行识别,并且可以安全进行数据输

  • SSL/TLS认证方式(采用http2协议)
  • 基于Token的认证方式(基于安全连接)
  • 不采用任何措施的连接,这是不安全的连接(默认采用http1)
  • 自定义的身份认证

TLS认证

通过加入证书,实现安全调用
TLS (Transport Layer Security,安全传输层),TLS 是建立在传输层 TCP 协议之上的协议,服务于应用层,前身是SSL (Secure Socket Layer,安全套接字层),实现了将应用层的报文进行加密后再交由 TCP 进行传输的功能。
TLS 协议主要解决如下三个网络安全问题:
1)保密(message privacy):保密通过加密 encryption 实现,所有信息都加密传输,第三方无法嗅探
2)完整性(message integrity):通过 MAC 校验机制,一旦被篡改,通信双方会立刻发现
3)认证(mutual authentication):双方认证,双方都可以配备证书,防止身份被冒充

证书:

  • key:服务器上的私钥文件,用于对发送给客户端数据的加密,以及对从客户端接收到数据的解密
  • csr:证书签名请求文件,用于提交给证书颁发机构(CA)对证书签名
  • crt:由证书颁发机构(CA)签名后的证书,或者是开发者自签名的证书,包含证书持有人的信息、持有人的公钥,以及签署者的签名等信息
  • pem:是基于 Base64 编码的证书格式,扩展名包括 PEM、CRT 和 CER
# 1. 生成私钥
# genrsa 加密算法格式
# 输出文件名 server.key
openssl genrsa -out server.key 2048

# 2. 生成证书 全部回车即可,可以不填
# 通过server.key 生成 server.crt 的证书 证书有效期
openssl req -new -x509 -key server.key -out server.crt -days 36500
# 国家名称
Country Name (2 letter code) [AU]:CN
# 省名称
State or Province Name (full name) [Some-State]:GuangDong
# 城市名称
Locality Name (eg, city) []:Meizhou
# 公司组织名称
Organization Name (eg, company) [Internet Widgits Pty Ltd]:Xuexiangban
# 部门名称
Organizational Unit Name (eg, section) []:go
# 服务器或网站名称
Common Name (e.g. server FQDN or YOUR name) []:kuangstudy
# 邮件
Email Address []:24736743@qq.com

# 3. 生成 csr
# 证书签名请求文件,提交给CA对证书签名
openssl req -new -key server.key -out server.csr

# 更改openssl.cnf(Linux是openssl.cfg)
# 1)复制一份你安装的openssl的bin目录里面的openssl.cnf文件到你项目所在的目录
# 2)找到[CA_default],打开copy_extensions = copy(就是把前面的#去掉)
# 3)找到[req],打开req_extensions = v3_req # The extensions to add to a certificate request
# 4)找到[v3_req],添加subjectAltName = @alt_names
# 5)添加新的标签[alt_names],和标签字段
# 指定域名,只能在该域名中访问
DNS.1 = *.kuangstudy.com

# 生成证书私钥test.key
openssl genkey -algorithm RSA -out test.key
# 新版的openSSL运行
openssl genrsa -out test.key 2048

# 通过私钥test.key生成证书请求文件test.csr(注意cfg和cnf)
# 官方的是cnf,根据自己的注意修改为cfg
openssl req -new -nodes -key test.key -out test.csr -days 3650 -subj "/C=cn/OU=myorg/O=mycomp/CN=myname" -config ./openssl.cnf -extensions v3_req

# test.csr是上面生成的证书请求文件。ca.crt/server.key是CA证书文件和key,用来对test.csr进行签名认证。这两个文件在第一部分生成。

# 生成SAN证书pem
# 通过签发证书test.csr,生成test.pem,颁发证书server.crt和对应的私钥server.key
openssl x509 -req -days 365 -in test.csr -out test.pem -CA server.crt -CAkey server.key -CAcreateserial -extfile ./openssl.cnf -extensions v3_req

生成私钥
在这里插入图片描述
生成证书
在这里插入图片描述
生成证书签名
在这里插入图片描述
在这里插入图片描述
在这里插入图片描述
服务端传入证书和私钥

func main() {
	// TLS认证
	// 自签名证书文件和私钥文件
	creds, _ := credentials.NewServerTLSFromFile("G:\\goProject\\src\\grpc\\key\\test.pem",
		"G:\\goProject\\src\\grpc\\key\\test.key")
	// 开启端口监听
	listen, _ := net.Listen("tcp", ":8080")
	// 创建grpc服务
	grpcServer := grpc.NewServer(grpc.Creds(creds))
	// 注册服务
	pb.RegisterSayHelloServer(grpcServer, &server{})
	// 启动服务
	err := grpcServer.Serve(listen)
	if err != nil {
		panic(err)
	}
}

正确传入证书和域名

func main() {
	// 客户端只需要传入证书
	// 访问域应该通过url获取,进行一个校验
	creds, _ := credentials.NewClientTLSFromFile("G:\\goProject\\src\\grpc\\key\\test.pem",
		"*.yfqc.com")
	// 创建grpc连接,不进行加密验证传输
	//conn, _ := grpc.NewClient(":8080", grpc.WithTransportCredentials(insecure.NewCredentials()))

	// 加密验证传输
	conn, _ := grpc.NewClient(":8080", grpc.WithTransportCredentials(creds))
	defer func(conn *grpc.ClientConn) {
		err := conn.Close()
		if err != nil {

		}
	}(conn)

	// 建立连接
	client := pb.NewSayHelloClient(conn)

	// 执行rpc调用(在服务器实现并返回结果)
	resp, _ := client.SayHello(context.Background(), &pb.HelloRequest{RequestName: "yf"})
	fmt.Println(resp.GetResponseMsg())
}

在这里插入图片描述
客户端不使用证书
在这里插入图片描述

// 传入无法匹配的域名
creds, _ := credentials.NewClientTLSFromFile("G:\\goProject\\src\\grpc\\key\\test.pem",
		"*.baidu.com")

在这里插入图片描述

Token认证

type PerRPCCredentials interface {
	// 获取元数据信息,客户端提过的键值对,context控制超时和取消,url请求入口的url
    GetRequestMetadata(ctx context.Context, uri ...string) (map[string]string, error)
    // 是否基于TLS认证进行安全传输,true必须加入TLS验证
    RequireTransportSecurity() bool
}

自定义gRPC认证

type ClientTokenAuth struct {
}

func (c ClientTokenAuth) GetRequestMetadata(ctx context.Context, uri ...string) (map[string]string, error) {
	return map[string]string{
		"appId":  "yfqc",
		"appKey": "123456",
	}, nil
}

func (c ClientTokenAuth) RequireTransportSecurity() bool {
	return false
}
func main() {
	// 客户端只需要传入证书
	// 访问域应该通过url获取,进行一个校验
	//creds, _ := credentials.NewClientTLSFromFile("G:\\goProject\\src\\grpc\\key\\test.pem",
	//	"*.yfqc.com")

	var opts []grpc.DialOption
	// 不进行加密传输
	opts = append(opts, grpc.WithTransportCredentials(insecure.NewCredentials()))
	// 添加Token认证,自定义gRPC认证
	opts = append(opts, grpc.WithPerRPCCredentials(new(ClientTokenAuth)))
	conn, _ := grpc.NewClient(":8080", opts...)

	// 创建grpc连接,不进行加密验证传输
	//conn, _ := grpc.NewClient(":8080", grpc.WithTransportCredentials(insecure.NewCredentials()))

	// 加密验证传输
	//conn, _ := grpc.NewClient(":8080", grpc.WithTransportCredentials(creds))
	defer func(conn *grpc.ClientConn) {
		err := conn.Close()
		if err != nil {

		}
	}(conn)

	// 建立连接
	client := pb.NewSayHelloClient(conn)

	// 执行rpc调用(在服务器实现并返回结果)
	resp, _ := client.SayHello(context.Background(), &pb.HelloRequest{RequestName: "yf"})
	fmt.Println(resp.GetResponseMsg())
}

服务端token校验

// grpc中定义的结构体
// type UnimplementedSayHelloServer struct{}
type server struct {
	// 继承文件中的方法
	pb.UnimplementedSayHelloServer
}

// SayHello 重写方法
func (s *server) SayHello(ctx context.Context, req *pb.HelloRequest) (*pb.HelloResponse, error) {
	// 获取元数据信息,进行拦截器模拟
	// 客户端传入的内容都可以在ctx中获取
	md, ok := metadata.FromIncomingContext(ctx)
	if !ok {
		return nil, errors.New("获取元数据失败")
	}
	var appId, appKey string
	if v, ok := md["appid"]; ok {
		appId = v[0]
	}
	if v, ok := md["appkey"]; ok {
		appKey = v[0]
	}
	// 通过数据库中的数据进行校验
	if appId != "yfqc" || appKey != "123456" {
		return nil, errors.New("token不正确")
	}

	return &pb.HelloResponse{
		ResponseMsg: "hello " + req.RequestName,
	}, nil
}

func main() {
	// TLS认证
	// 自签名证书文件和私钥文件
	//creds, _ := credentials.NewServerTLSFromFile("G:\\goProject\\src\\grpc\\key\\test.pem",
	//	"G:\\goProject\\src\\grpc\\key\\test.key")
	// 开启端口监听
	listen, _ := net.Listen("tcp", ":8080")
	// 创建grpc服务
	grpcServer := grpc.NewServer(grpc.Creds(insecure.NewCredentials()))
	//grpcServer := grpc.NewServer(grpc.Creds(creds))
	// 注册服务
	pb.RegisterSayHelloServer(grpcServer, &server{})
	// 启动服务
	err := grpcServer.Serve(listen)
	if err != nil {
		panic(err)
	}
}

在这里插入图片描述
可以使用多种凭证组合

JsonRPC

轻量级远程过程调用协议,使用JSON作为数据格式
请求对象

{
		"jsonrpc": "2.0", // 版本号
		"method": "subtract", // 要调用的远程方法名称
		"params": [42, 23], // 结构化值,可以是数组或对象,传递给远程方法的参数
		"id": 1 // 唯一标识符,可能是字符串或数字,用于关联请求和响应,服务端必须返回相同的值。请求时通知类型可以省略
}

响应对象

// 成功
{
		"jsonrpc": "2.0",
		"result": 19, // 请求成功时,包含由远程方法返回的结果,请求失败时,不包含此成员
		"id": 1 // 与请求id相同
}
// 失败
{
		"jsonrpc": "2.0",
		"error": {
				"code": -32601, // 错误码
				"message": "Method not found" // 错误简短描述
				// data 可选,包含额外的错误信息
		}, // 请求失败时,包含一个错误对象,请求成功不包含此成员
		"id": 1
}
  • -32700: 解析错误,服务器收到无效的 JSON。
  • -32600: 无效请求,发送的 JSON 不是有效的请求对象。
  • -32601: 方法未找到,方法不存在或无效。
  • -32602: 无效参数,提供的参数无效。
  • -32603: 内部错误,JSON-RPC 内部错误。

对比分析

gRPC相比JsonRPC性能更高
协议:
gRPC使用protobuf作为默认的序列化格式,是一种二进制协议
JsonRPC使用Json文本格式,虽然可读性更强,但在网络传输和解析过程中消耗资源更多
protobuf在序列化和反序列化速度上更快,数据体积更小
传输:
gRPC基于HTTP/2协议,支持多路复用、头部压缩等特性,可以减少连接建立的开销,提高并发处理能力,多路复用可以在同一个TCP连接上传输多个请求和响应
JsonRPC基于HTTP/1.1,每个请求都需要建立一个新连接,效率相对较低
代码生成:
gRPC通过protobuf定义服务接口,使用工具自动生成客户端和服务端代码,减少手动编写代码的工作量,同时避免人为错误
JsonRPC需要手动处理请求和响应的序列化和反序列化,容易出错

在简单场景下,JsonRPC由于其简单易懂的特性,可能更易于调试和集成。在网络环境不稳定时,HTTP/2的多路复用特性反而可能成为瓶颈

评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值