本文使用grpc协议
微服务导读
使用gRPC一个完整的调用过程如下
1.客户端调用A方法,发起RPC调用
2.客户端对请求信息、参数使用Protobuf进行对象序列化压缩
3.服务端接收到请求后,解码请求体,进行业务逻辑处理并返回。
4.服务端对响应结果使用Protobuf进行对象序列化压缩
5.客户端接受到服务端响应,解码请求体。回调被调用的A方法,唤醒正在等待响应(阻塞)的客户端调用并返回响应结果
前4步为重要的请求周期,这个请求过程不在通过http方式运行,客户端与服务端的通讯参数,通过protocol buff通讯,而.proto文件定义了编码解码的规范,犹如一把钥匙(交互文档)一样。
gRPC的支持请求响应参数一元传输(一次性传递完)、流式传输(持续的流式传输)
示例 user.proto
syntax = "proto3";
package user;
// protoc-gen-go 版本大于1.4.0, proto文件需要加上go_package,否则无法生成
option go_package = "core/user";
message IdRequest {
int32 id = 1;
}
message UserResponse {
// 用户id
int32 id = 1;
// 用户名称
string name = 2;
}
message UserOauthResponse {
int32 id = 1;
string open_id = 2;
string nickname = 3;
string avatar = 4;
}
service User {
rpc getUser(IdRequest) returns(UserResponse);
rpc getUserOauth(IdRequest) returns(UserOauthResponse);
}
定义了两个rpc服务getUser,getUserOauth,其中定义了请求、返回参数
交互过程中,此文件可代替对接文档,下放调用者即可
基础依赖
gRPC通讯依赖Protocol Buffers(简称 protobuf),需安装3个软件包protoc
,protoc-gen-go
,protoc-gen-go-grpc
,作用分别是:
- protoc 是 Protocol Buffers 的编译器,它将
.proto
文件编译成多种编程语言的源代码,包括 C++、Java、Python、Go 等。它可以根据.proto
文件生成对应语言的数据类型定义和序列化/反序列化方法等代码。
官方仓库下载 https://github.com/protocolbuffers/protobuf/releases 直接下编译好的zip包到本地,可执行文件放入环境变量中
wancheng@MacBook-Pro-4 www % protoc --version
libprotoc 3.12.4
- protoc-gen-go 是用于生成 Go 语言代码的插件,它
是 protoc 的一个插件
。当使用 protoc 编译 .proto 文件时,可以指定使用 protoc-gen-go 插件来生成 Go 语言代码。生成的 Go 代码包括 Protocol Buffers 的消息类型定义、序列化/反序列化方法、以及一些辅助函数等。
通过命令 go install google.golang.org/protobuf/cmd/protoc-gen-go@v1.26 应确保GOPATH/bin目录在环境变量中
这个程序本质是一个插件,没有进行命令行单独运行,需搭配protoc命令,因此无法通过 `protoc-gen-go --version` 确认安装成功
- protoc-gen-go-grpc 是用于生成 Go 语言 gRPC 服务接口代码的插件,它
也是 protoc 的一个插件
。它可以根据 .proto 文件生成 gRPC 服务接口的定义和实现,包括服务接口中的方法定义、请求和响应消息类型、以及服务端和客户端的 stub 代码等。
通过命令 go install google.golang.org/grpc/cmd/protoc-gen-go-grpc@v1.2 应确保GOPATH/bin目录在环境变量中
wancheng@MacBook-Pro-4 www % protoc-gen-go-grpc --version
protoc-gen-go-grpc 1.2.0
由此看出,protoc命令是基础命令,protoc-gen-go命令是解读.proto文件生成序列化编码解码、操作数据的插件,protoc-gen-go-grpc命令是做服务通讯方面的插件
基础使用
命令执行
protoc --go_out=./user --go_opt=paths=source_relative --go-grpc_out=./user --go-grpc_opt=paths=source_relative user.proto
参数含义
--go_out=. 使用 protoc-gen-go 插件将 user.proto 文件编译为 Go 语言代码,并将生成的代码放置在 ./user目录下
--go_opt=paths=source_relative 设置生成的 Go 代码中的导入路径为相对路径
--go-grpc_out=. 使用 protoc-gen-go-grpc 插件将 user.proto 文件编译为带有 gRPC 服务的 Go 语言代码,并将生成的代码放置在./user目录下
--go-grpc_opt=paths=source_relative 设置生成的带有 gRPC 服务的 Go 代码中的导入路径为相对路径
上面命令在user目录下生成了两个文件user_pb.go
,user_grpc.pb.go
- user_pb.go 包含了 Protobuf 文件中定义的所有消息类型、服务以及相关的方法。服务端需要使用这个文件来实现服务器端的业务逻辑。
服务端、客户端均可通过此文件中封装好的方法,操作传输的数据
此文件不建议修改,已满足大部分数据的增删改查
- user_grpc.pb.go 包含了 gRPC 相关的代码,比如客户端需要使用的 Client 类型以及服务器端需要实现的 Server 接口。客户端需要使用这个文件来调用服务器端提供的服务
服务端、客户端均可通过此文件中封装好的方法,创建rpc服务端,rpc请求客户端
此文件不建议修改
上述两个文件,为最原始的gRPC调用方法
一个完整客户端代码如下
当我们客户端拿到服务端定义的.proto文件后,通过protoc命令生成上面两个核心代码文件,调用其方法
conn, _ := grpc.Dial("127.0.0.1:8080", grpc.WithInsecure()) // "google.golang.org/grpc"
client := user.NewUserClient(conn)
sendParams := &user.IdRequest{Id: 5}
response, _ := client.GetUser(context.Background(), sendParams)
fmt.Println(response)
这种方式是最简单的例子,客户端直链服务端的地址,未引入etcd服务发现等概念,下面,我们通过go-zero框架,重新来实现一个完整的调用
使用go-zero 依赖安装
goctl是干嘛的不在做过多说明
goctl为我们提供了一键安装方法,免去很多步骤,上诉的3个命令可通过下面一条命令执行即可,无论之前是否安装过,也不怕
goctl env check --install --verbose --force
检查是否成功
goctl env check --verbose
[goctl-env]: preparing to check env
[goctl-env]: looking up "protoc"
[goctl-env]: "protoc" is installed
[goctl-env]: looking up "protoc-gen-go"
[goctl-env]: "protoc-gen-go" is not found in PATH
[goctl-env]: looking up "protoc-gen-go-grpc"
[goctl-env]: "protoc-gen-go-grpc" is installed
是不是感觉很方便,下面我们来做具体业务
使用go-zero框架开发gRPC
在工作目录下,新建一个rpc项目 servjj
# 这个命令会新建一个空的gRPC项目,并为我们准备了一个最简单的demo示例
goctl rpc new servjj
初始化项目
cd servjj && go mod tidy
为了避免混淆,我们把demo给删掉,目录结构如下
├── etc 配置文件目录,存放 .yaml文件
├── go.mod
├── go.sum
├── internal 业务文件夹,大部分编码工作的地方
│ ├── config
│ │ └── config.go 配置文件加载类
│ ├── logic 业务逻辑层
│ └── svc
│ └── servicecontext.go 项目初始化上下文类
└── proto 此目录为自己建的,专门用来存放.proto文件
└── user.proto 还是用上面的.proto文件做演示
这是个空项目,main文件和配置文件都没有,通过下面的命令,可以初始化服务文件
需要注意的是,.proto 文件中package user;
为整个微服务的名称主体,goctl会生成一个对应的 user.go (main包)文件和一个 对应的 user.ymal配置文件,如项目服务过多,可通过import方式管理多个.proto文件
在go-zero框架中生成user.proto的业务操作代码
goctl rpc protoc proto/user.proto --go_out=./core --go-grpc_out=./core --zrpc_out=. --style=goZero
生成代码文件,将原生的protoc命令创建的操作protobuff的文件放入core文件夹,(–zrpc_out)go-zero进行封装的代码放入当前目录下,代码风格为小驼峰
user.proto文件中,定义了两个调用方法getUser,getUserOauth,传入用户id返回用户信息
现在的目录结构为
├── core
│ └── user 通过user.proto文件生成的原生方法
│ ├── user.pb.go
│ └── user_grpc.pb.go
├── etc
│ └── user.yaml user服务配置文件
├── go.mod
├── go.sum
├── internal
│ ├── config
│ │ └── config.go
│ ├── logic user.proto中声明的两个外界调用方法,业务逻辑
│ │ ├── getUserLogic.go
│ │ └── getUserOauthLogic.go
│ ├── server
│ │ └── userServer.go 服务的调用方法handler方法,无需动此文件
│ └── svc
│ └── servicecontext.go
├── proto
│ └── user.proto
├── user.go main主文件,创建服务端开启运行
└── userclient 客户端的(调用方法,需传入客户端链接具柄)封装,本文演示的是服务端,无需关注
└── user.go
配置文件示例
我们的项目名为servjj.rpc,rpc服务端监听的端口为8080
Name: servjj.rpc
ListenOn: 0.0.0.0:8080
go-zero框架中封装了etcd注册发现功能,仅仅需添加配置即可
Etcd:
Hosts:
- 127.0.0.1:2379
Key: user.rpc
# User: root
# Pass: "123456"
etcd的key为user.rpc
,用来标识此项目产生的key,无论是否使用了etcd注册,上面的ListenOn地址都是可以直链的
配置文件最终为 user.ymal
Name: servjj.rpc
ListenOn: 0.0.0.0:8080
Etcd:
Hosts:
- 127.0.0.1:2379
Key: user.rpc
# User: root
# Pass: "123456"
如若不使用etcd服务注册,配置文件如下
Name: servjj.rpc
ListenOn: 0.0.0.0:8080
配置文件加载类 config/config.go 无需做任何改动
type Config struct {
zrpc.RpcServerConf
}
服务配置文档如下 https://go-zero.dev/docs/tutorials/grpc/server/configuration
服务端逻辑编写
internal/logic/getUserLogic.go
func (l *GetUserLogic) GetUser(in *user.IdRequest) (*user.UserResponse, error) {
return &user.UserResponse{
Id: in.Id,
Name: fmt.Sprint("来自rpc服务器", l.svcCtx.Config.RpcServerConf.ListenOn, "返回的名字"),
Gender: 1,
}, nil
}
internal/logic/getUserOauthLogic.go
func (l *GetUserOauthLogic) GetUserOauth(in *user.IdRequest) (*user.UserOauthResponse, error) {
return &user.UserOauthResponse{
Id: in.Id,
Nickname: fmt.Sprint("来自rpc服务器", l.svcCtx.Config.RpcServerConf.ListenOn, "返回的名字"), // 注意,这里故意这样写,以方便演示,让客户端知道是哪个后端节点返回的请求
Avatar: "",
}, nil
}
我们把这上面的user.proto定义的两个远程方法,逻辑已编写完成,到目前为止,rpc服务端开发编码工作已全部完成
修改user.proto文件,再添加一个方法
message UserNameResponse { // 新增
// 用户id
int32 id = 1;
// 用户名称
string name = 2;
}
service User {
rpc getUser(IdRequest) returns(UserResponse);
rpc getUserOauth(IdRequest) returns(UserOauthResponse);
rpc getUserName(IdRequest) returns(UserNameResponse); // 新增
}
再次执行命令,不会覆盖之前的代码
goctl rpc protoc proto/user.proto --go_out=./core --go-grpc_out=./core --zrpc_out=. --style=goZero
目录结构小微变化
├── core
│ └── user
│ ├── user.pb.go
│ └── user_grpc.pb.go // 代码更新,新增getUserName方法实现
├── etc
│ └── user.yaml
├── go.mod
├── go.sum
├── internal
│ ├── config
│ │ └── config.go
│ ├── logic
│ │ ├── getUserLogic.go
│ │ ├── getUserNameLogic.go // 新增getUserName方法逻辑文件
│ │ └── getUserOauthLogic.go
│ ├── server
│ │ └── userServer.go // 新增getUserName方法handler
│ └── svc
│ └── servicecontext.go
├── proto
│ └── user.proto
├── user.go
└── userclient
└── user.go // 新增getUserName方法
部署
开启etcd服务
不在过多说明etcd的安装开启,参考如下
# 创建容器并启动
docker run -d --name etcd-server --network app-tier --publish 2379:2379 --publish 2380:2380 --env ALLOW_NONE_AUTHENTICATION=yes --env ETCD_ADVERTISE_CLIENT_URLS=http://etcd-server:2379 bitnami/etcd:latest
推荐一个etcd的GUI客户端 http://etcdmanager.io/
代码部署
我们以同一个项目不同端口运行,来模拟多个节点运行
节点1:
修改user.yaml
文件
ListenOn: 0.0.0.0:8081
新开1个终端,执行 go run user.go
此刻,etcd存储的值为
key | value |
---|---|
user.rpc/7587872091812852488 | 10.0.89.75:8081 |
10.0.89.75 是我本机的局域网ip,等价于 127.0.0.1
节点2:
修改user.yaml
文件
ListenOn: 0.0.0.0:8082
重新开1个终端,执行 go run user.go
此刻,etcd存储的值为
key | value |
---|---|
user.rpc/7587872091812852488 | 10.0.89.75:8081 |
user.rpc/7587872091812852496 | 10.0.89.75:8082 |
到此,服务端工作已全部完毕~
测试
本文使用postman充当客户端测试,不再使用程序,由于上面我们是通过etcd部署的,但postman不支持链接etcd服务发现功能,故使用postman直链测试
postman 10.16.0
-
创建一个grpc的文件夹
-
在此文件夹下新建一个gRPC服务
-
填入RPC服务端地址,倒入
user.proto
文件
-
选中指定的User服务
选中User
选项,点击Import as API
按钮
-
选择一个具体rpc方法发送请求
本文使用了etcd进行后端部署,下文中,将介绍客户端通过etcd进行服务发现调用rpc方法
源码 https://gitee.com/qq_connect-60293/servjj