gRPC 笔记(03)— protobuf 文件编写、编译器安装、生成客户端和服务端示例
2. Protobuf 文件编写2.1 安装 protoc 编译器将 Proto 协议文件转换为多种语言对应格式的工具,根据对应平台选择对应的安装包,安装包下载地址https://github.com/protocolbuffers/protobuf/releasescd ~/tmp# 下载wget https://github.com/protocolbuffers/protobuf/relea
1. protoc 编译器安装
1.1 二进制安装
将 Proto
协议文件转换为多种语言对应格式的工具,根据对应平台选择对应的安装包,安装包下载地址 https://github.com/protocolbuffers/protobuf/releases
cd ~/tmp
# 下载
wget https://github.com/protocolbuffers/protobuf/releases/download/v3.11.4/protoc-3.11.4-linux-x86_64.zip
# 解压后得到 bin 目录下的 protoc
unzip protoc-3.11.4-linux-x86_64.zip
# 创建存放 protoc 目录
sudo mkdir /usr/local/protobuf
# 复制 protoc 到刚刚创建的目录下
sudo cp bin/protoc /usr/local/protobuf/
添加 protoc
环境变量
vim /etc/profile
# 在文件末尾修改
PATH=$PATH:/usr/local/php/bin:/usr/local/protobuf
# 使其修改生效
source /etc/profile
1.2 apt 安装
sudo apt install protobuf-compiler
sudo apt install protobuf-compiler-grpc
查看是否安装成功,
protoc --version
libprotoc 3.11.4
2. 编写 Protocol Buffers 文件
无论使用何种语言创建客户端和服务端,都依照相同的 Proto
文件定义的协议接口和数据格式,客户端和服务器端都会使用由服务定义产生的接口代码。
protocol buffers
可以定义消息类型和服务类型:
- 消息包含字段,每个字段由其类型和唯一索引值进行定义;
- 服务则包含方法,每个方法由其类型、输入参数和输出参数进行定义;
2.1 定义消息类型
消息 message
是客户端和服务器端交换的数据结构。GetUserRequest
消息类型的 protocol buffers
定义:
// 定义 GetUser 请求消息格式或类型
message GetUserRequest {
int64 userid = 1; // 记录用户编号,具有唯一的字段编号,该编号用来在二进制格式消息中识别字段
}
我们可以自定义包含字符串字段的消息类型,或使用 protocol buffers
库所提供的较为流行的消息类型 google.protobuf.StringValue
。
GetUserResponse
消息类型的 protocol buffers
定义:
// GetUser 响应结构
message GetUserResponse {
int64 userid = 1;
string username = 2;
UserSex sex = 3;
}
这里为每个消息字段所分配的数字用来在消息中标识该字段。因此,在同一个消息定义中,不能为两个字段设置相同的数字。
2.2 定义服务类型
服务 service
是暴露给客户端的远程方法集合,按照 protocol buffers
的规则,远程方法只能有一个参数,并只能返回一个值。如果需要给方法传递多个值,就要定义一个消息类型,并对所有的值进行分组,就像在 GetUserResponse
消息类型中所做的那样。
// 定义 User 服务接口
service User {
// 定义 GetUser 方法 - 获取某个 user 数据,入参为 GetUserRequest 定义的数据结构
// 返回值为 GetUserResponse 定义的数据结构
rpc GetUser(GetUserRequest) returns (GetUserResponse) {}
// 定义 GetUserList 方法 - 获取 user 所有数据
rpc GetUserList(GetUserListRequest) returns (UserListResponse) {}
}
2.3 消息和服务的完整定义
将消息和服务组合到一起,就有了完整 protocol buffers
定义。user.proto
文件内容如下:
syntax = "proto3"; // 服务定义首先声明所使用的 protocol buffers 版本
package user_proto; // 用来防止协议消息类型之间发生命名冲突的包名,该包名也会用来生成代码
// 下面代码为了解决 protoc-gen-go: unable to determine Go import path for "user.proto"
/*
option go_package = "path;name";
path 表示生成的go文件的存放地址,会自动生成目录的。
name 表示生成的go文件所属的包名
*/
option go_package = "./;user_proto";
// 定义 User 服务接口
service User {
// 定义 GetUser 方法 - 获取某个 user 数据,入参为 GetUserRequest 定义的数据结构
// 返回值为 GetUserResponse 定义的数据结构
rpc GetUser(GetUserRequest) returns (GetUserResponse) {}
// 定义 GetUserList 方法 - 获取 user 所有数据
rpc GetUserList(GetUserListRequest) returns (UserListResponse) {}
}
// 枚举类型第一个字段必须为 0
enum UserSex {
MEN = 0;
WOMEN = 1;
}
// 定义 GetUser 请求消息格式或类型
message GetUserRequest {
int64 userid = 1; // 记录用户编号,具有唯一的字段编号,该编号用来在二进制格式消息中识别字段
}
// GetUser 响应结构
message GetUserResponse {
int64 userid = 1;
string username = 2;
UserSex sex = 3;
}
// GetUserList 请求结构
message GetUserListRequest {}
// 响应结构
message UserListResponse {
// repeated 重复(数组)
repeated GetUserResponse list = 1;
}
服务 service
就是可被远程调用的一组方法,比如 GetUser
方法和 GetUserList
方法。每个方法都有输入参数和返回类型,既可以被定义为服务的一部分,也可以导入 protocol buffers
定义中。
输入参数和返回参数既可以是用户定义类型,比如 GetUserRequest
类型和 GetUserListRequest
类型,也可以是服务定义中已经定义好的 protocol buffers
已知类型。这些类型会被构造成消息,每条消息都是包含一系列名–值对信息的小型逻辑记录,这些名–值对叫作字段。这些字段都是具有唯一编号的名–值对(如 string id = 1
),在二进制形式消息中,可以用编号来识别相应字段。
在 protocol buffers
定义中,可以指定包名如 user_proto
这样做能够避免在不同的项目间出现命名冲突。当使用这个包属性生成服务或客户端代码时,除非明确指明了不同的包名,否则将为对应的编程语言生成相同的包。当然,该语言需要支持包的概念。
在定义包名的时候,还可以使用版本号,如 user_proto.v1
这样一来,未来对 API 的主要变更就可以在相同的代码库中共存。
2.4 导入其他 proto 消息类型
如果需要使用其他 proto
文件中定义的消息类型,那么可以将它们导入到当前的 protocol buffers
定义中。
如要使用 wrappers.proto
文件中的 StringValue
类型 google.protobuf.StringValue
就可以按照如下方式在定义中导入 google/protobuf/wrappers.proto
文件:
syntax = "proto3";
import "google/protobuf/wrappers.proto";
package ecommerce;
3. 安装依赖库
3.1 安装 gRPC 库
go get google.golang.org/grpc
3.2 安装 protoc 插件
要让 user.proto
生成 Go
文件,需要 protoc-gen-go
所以要下载:
go get github.com/golang/protobuf/protoc-gen-go
在 bin
目录下会生成一个 protoc-gen-go
可执行文件,就是用于生成 Go
文件的。
4. 编写服务端流程
首先创建一个为 go-grpc
的项目:
mkdir ~/go-grpc
设置 Go
模块代理,因为我们要使用 Go modules
第三方包的依赖管理工具,当然了你的 Go
环境最好是 1.13 以上。
go env -w GOPROXY=https://goproxy.cn,direct
4.1 初始化这个项目
我们使用 Go modules
来初始化(创建)这个项目,毕竟是以后的主流了
cd ~/go-grpc
go mod init go-grpc
下载项目所使用的包,它们之间的依赖由 Go modules
帮我们完成了,记住一定要在项目下打开命令行下执行:
go get github.com/golang/protobuf
go get google.golang.org/grpc
创建 user_proto
目录,将刚刚编写的 user.proto
放进来:
go-grpc
├── go.mod
├── go.sum
└── user_proto
└── user.proto
生成 Go
文件,这里用了 plugins
选项,提供对 gRPC
的支持,否则不会生成 Service
的接口,方便编写服务器和客户端程序:
cd ~/go-grpc/user_proto
protoc --go_out=plugins=grpc:. user.proto
# 或者
protoc -I=./ --go_out=plugins=grpc:. ./user.proto
根据编译指令,编译成对应语言的代码文件:
protoc -I=$SRC_DIR --xxx_out=$DST_DIR $SRC_DIR/xxx.proto
通过 --proto_path
或 -I
命令行标记来指定源 proto
文件和依赖的 proto
文件的目录路径
$SRC_DIR
:存放协议源文件的目录地址;$DST_DIR
:输出代码文件的目录地址;xxx.proto
:协议源文件名称;–xxx_out
:根据自己的需要,选择对应的语言,例如(Java:–java_out
,C++:–cpp_out
等);- 可通过在命令提示符中输入
protoc --help
查看更多帮助。
查看目录:
go-grpc
├── go.mod
├── go.sum
└── user_proto
├── user.pb.go
└── user.proto
4.2 创建服务端
让 UserServer
服务工作有两个部分:
- 实现我们服务定义的生成服务接口,做我们服务的实际工作
- 运行一个
gRPC
服务器,监听来自客户端的请求并返回服务的响应
mkdir ~/go-grpc/server
cd ~/go-grpc/server
我们首先实现 user.pb.go
中的 UserServer
(该接口是自动生成的),即我们服务的实际工作接口:
// UserServer is the server API for User service.
type UserServer interface {
// 定义 GetUser 方法
GetUser(context.Context, *GetUserRequest) (*GetUserResponse, error)
// 定义 GetUserList 方法
GetUserList(context.Context, *GetUserListRequest) (*UserListResponse, error)
}
创建 user.go
来实现 UserServer
接口,即我们实际的工作服务实现:
package main
import (
"context"
"strconv"
// 引入 proto 编译生成的包
pb "rpcDemo/user_proto"
)
// 定义 User 并实现约定的接口, User 结构体是对服务器的抽象。可以通过它将服务方法附加到服务器上
type User struct {
UserId int64 `json:"user_id"`
UserName string `json:"user_name"`
}
// 获取某个 user 数据,入参为 GetUserRequest,返回值为 GetUserResponse,
// 它们都在 user.pb.go 文件中定义,该文件是通过 user.proto 文件自动生成的
func (u *User) GetUser(ctx context.Context, in *pb.GetUserRequest) (*pb.GetUserResponse, error) {
// 待返回数据结构
resp := new(pb.GetUserResponse)
resp.Userid = in.Userid
resp.Username = "wohu"
resp.Sex = pb.UserSex_MEN
return resp, nil
}
// 获取 user 所有数据
func (u *User) GetUserList(ctx context.Context, in *pb.GetUserListRequest) (*pb.UserListResponse, error) {
list := make([]*pb.GetUserResponse, 0, 3)
for i := 1; i <= 3; i++ {
list = append(list, &pb.GetUserResponse{Userid: int64(i), Username: "wohu" + strconv.Itoa(i), Sex: pb.UserSex_MEN})
}
// 待返回数据结构
resp := new(pb.UserListResponse)
resp.List = list
return resp, nil
}
/*
这两个方法都有一个 Context 参数。Context 对象包含一些元
数据,比如终端用户授权令牌的标识和请求的截止时间。这些元数
据会在请求的生命周期内一直存在。
*/
现在我们开始编写对外服务 main.go
,以便客户端可以实际使用我们的服务:
- 创建监听
listener
- 创建
gRPC
的服务 - 将我们的服务注册到
gRPC
的Server
中 - 启动
gRPC
服务,将我们自定义的监听信息传递给gRPC
客户端
package main
import (
"log"
"net"
// 引入 proto 编译生成的包
pb "rpcDemo/user_proto"
"google.golang.org/grpc"
)
func main() {
// 监听地址和端口
listen, err := net.Listen("tcp", ":50051")
if err != nil {
log.Fatalf("监听端口失败: %v", err)
}
// 实例化 grpc Server
serverGrpc := grpc.NewServer()
// 通过调用生成的 API,将之前生成的服务注册到新创建的 gRPC 服务器上。注册 User service
pb.RegisterUserServer(serverGrpc, &User{})
log.Println("开始监听 Grpc 端口 0.0.0.0:50051")
// 启动服务
err = serverGrpc.Serve(listen)
if err != nil {
log.Println("启动 Grpc 服务失败")
}
}
查看目录:
go-grpc
├── go.mod
├── go.sum
├── server
│ ├── main.go
│ └── user.go
└── user_proto
├── user.pb.go
└── user.proto
我们回顾下:
- 首先要实现
UserServer
接口 - 创建
gRPC Server
对外端口 - 注册我们实现的
UserServer
接口的实例 - 最后调用
Serve()
启动我们的服务
4.3 运行服务端
$ go build -o ./bin/server
$ ls
bin main.go user.go
$ ./bin/server
开始监听 Grpc 端口 0.0.0.0:50051
5. 编写客户端流程
首先创建我们所需的目录:
mkdir ~/go-grpc/client
cd ~/go-grpc/client
5.1 初始化客户端
首先在连接我们建立好的服务端的 IP
和端口 main.go
,通过把服务器地址和端口号传递给 grpc.Dial()
来创建通道:
package main
import (
"context"
"encoding/json"
"log"
"net/http"
"strconv"
// 引入 proto 编译生成的包
pb "rpcDemo/user_proto"
"google.golang.org/grpc"
)
const (
// Address gRPC 服务地址
Address = "127.0.0.1:50051"
)
var UClient pb.UserClient
// 初始化 Grpc 客户端
func initGrpc() {
// 连接 GRPC 服务端
conn, err := grpc.Dial(Address, grpc.WithInsecure()) // 不安全的链接
if err != nil {
log.Fatalln(err)
}
// 初始化 User 客户端
UClient = pb.NewUserClient(conn)
log.Println("初始化 Grpc 客户端成功")
}
func GetUser(w http.ResponseWriter, r *http.Request) {
// 获取 GET 的参数
userid := r.FormValue("userid")
id, err := strconv.ParseInt(userid, 10, 0)
if err != nil {
w.Write([]byte("userid The parameters must be integers"))
return
}
// 调用 Grpc 的远程接口
data, err := UClient.GetUser(context.Background(), &pb.GetUserRequest{Userid: id})
if err != nil {
w.Write([]byte("Grpc: " + err.Error()))
return
}
// json 格式化
js, _ := json.Marshal(data)
w.Write(js)
}
func GetUserList(w http.ResponseWriter, r *http.Request) {
// 调用 Grpc 的远程接口
data, err := UClient.GetUserList(context.Background(), &pb.GetUserListRequest{})
if err != nil {
w.Write([]byte("Grpc: " + err.Error()))
return
}
// json 格式化
js, _ := json.Marshal(data.List)
w.Write(js)
}
// 启动 http 服务
func main() {
initGrpc()
http.HandleFunc("/user/get", GetUser)
http.HandleFunc("/user/list", GetUserList)
log.Println("开始监听 http 端口 0.0.0.0:8080")
err := http.ListenAndServe(":8080", nil)
if err != nil {
log.Printf("http.ListenAndServe err:%v", err)
}
}
5.2 运行客户端
$ go build -o ./bin/client
$ ls
bin main.go
$ ./bin/client
2022/04/28 11:51:40 初始化 Grpc 客户端成功
2022/04/28 11:51:40 开始监听 http 端口 0.0.0.0:8080
执行 GET 和 POST 请求
$ curl http://127.0.0.1:8080/user/get -d "userid=5"
{"userid":5,"username":"laixhe"}
$ curl http://127.0.0.1:8080/user/list
[{"userid":1,"username":"wohu1"},{"userid":2,"username":"wohu2"},{"userid":3,"username":"wohu3"}]
更多推荐
所有评论(0)