微服务架构
将大型应用程序拆分为多个小型、独立服务的架构模式。其核心特点包括:
- 服务拆分:将应用程序分解为多个小型服务,每个服务负责特定的业务功能 独立部署:每个服务可以独立开发、测试和部署,互不影响
- 轻量级通信:服务之间通过轻量级的通信机制(如HTTP/REST、gRPC等)进行交互,降低耦合度
- 技术多样性:不同服务可以使用不同的编程语言和技术栈,以适应不同的业务需求
- 高可用性:服务可以水平扩展,以应对高流量和高并发请求
造成问题:
- 代码冗余
- 服务和服务之间存在调用关系
服务拆分后是,服务和服务之间、进程和进程之间的调用
需要发起网络调用,使用http虽然便捷,但是在微服务中性能较低
RPC
一种进程间通信协议,允许程序调用另一台机器上的函数/过程
引入RPC(远程过程调用),通过自定义协议发起TCP调用,加快传输效率
RPC屏蔽分布式计算中的各种调用细节,可以像本地调用一样直接调用一个远程函数
底层通过序列化(编码)请求、网络传输、远程解码执行,再将结果返回
客户端与服务端沟通的过程:
客户端发送数据(字节流的形式)
服务端接收并解析,根据约定执行指定操作,把结果返回给客户端
RPC就是将上述过程进行封装,使操作更加优化
使用广泛认可的协议,使其规范化
RPC 的基本流程:
- 客户端(Client)通过本地调用的方式调用服务(以接口方式调用)
- 客户端存根(Client Stub)接收到调用请求后负责将方法、入参等信息进行组装序列化成能够进行网络传输的消息体(将消息体对象序列化为二进制流)
- 客户端存根(Client Stub)找到远程的服务地址,并且将消息通过网络发送给服务端(通过sockets发送消息)
- 服务端存根(Server Stub)收到消息后进行反序列化操作,即解码(将二进制流反序列化为消息对象)
- 服务端存根(Server Stub)通过解码结果调用本地的服务进行相关处理
- 服务端(Server)本地服务业务处理
- 服务端(Server)将处理结果返回给服务端存根
- 服务端存根(Server Stub)序列化处理结果(将结果消息对象序列化为二进制流)
- 服务端存根(Server Stub)将序列化结果通过网络发送至客户端(通过sockets发送消息)
- 客户端存根(Server Stub)接收到消息,进行反序列化解码(将结果二进制流反序列化为消息对象)
- 客户端得到最终的结果。
gRPC
高性能,开源的通用RPC框架,基于“服务定义”的思想
调用方:client
被调用方:server
通过某种方式描述服务,描述于语言无关,在”服务定义“的过程中,描述提供的服务服务名,可被调用的方法,方法的入参和回参
在定义好服务和方法后,gRPC屏蔽底层细节,client直接调用定义好的方法,就可以拿到预期的返回结果,在server端需要实现定义的方法,只需要实现定义方法的具体逻辑
双方约定好接口,server实现接口,client调用这个接口的代理对象
可以使用C++作为服务端,Golang,Java作为客户端,所以在”定义服务“和编码解码的过程中,应该与语言无关
Protocol Buffss
使用Protocol Buffss(一套成熟的数据结构序列化机制)将定义的方法,转换为特定语言的代码
例如:一种类型的参数,会帮忙转为Golang中的struct结构体,定义的方法转为func函数,在发送和接收请求的时候,会完成编码和解码的工作
将即将发送的数据编码为gRPC能够传输的形式,将接收到的数据解码为编程语言可以理解的数据格式
序列化:数据结构或对象转为二进制串的过程
反序列化:二进制串转为数据结构或对象的过程
protobuf
一种数据格式,适合高性能,对响应速度有要求的数据传输场景
二进制数据格式,需要编码和解码,本身不具有可读性
- 序列化后体积相比json,xml很小,适合网络传输
- 支持跨平台多语言
Protobuf 使用一种简单、语言无关的 .proto 文件来描述数据结构。这种文件就像是一个通用的 “模板”,它定义了消息(message)的结构,包括消息中包含的字段以及每个字段的类型(如 int32、string 等)和标签号(tag)
只需要根据这个 .proto 文件定义来生成对应语言的代码 - 消息格式升级和兼容性不错
- 序列化反序列化速度很快
安装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) {}
}
服务端编写
实现约束
- 创建gRPC Server 对象,Server端的抽象对象
- 将server(需要被调用的服务端接口) 注册到gRPC Server的内部注册中心
在接收到请求时,通过内部的服务发现,该服务端接口并转接进行逻辑处理 - 创建Listen,监听TCP端口
- 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)
}
*/
客户端编写
调用
- 创建与给定服务端的连接交互
- 创建server的客户端对象
- 发送RPC请求,等待同步响应,得到回调后返回响应结果
- 输出响应结果
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的多路复用特性反而可能成为瓶颈