gRPC 笔记(03)— protobuf 文件编写、编译器安装、生成客户端和服务端示例

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 服务工作有两个部分:

  1. 实现我们服务定义的生成服务接口,做我们服务的实际工作
  2. 运行一个 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,以便客户端可以实际使用我们的服务:

  1. 创建监听 listener
  2. 创建 gRPC 的服务
  3. 将我们的服务注册到 gRPCServer
  4. 启动 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

我们回顾下:

  1. 首先要实现 UserServer 接口
  2. 创建 gRPC Server 对外端口
  3. 注册我们实现的 UserServer 接口的实例
  4. 最后调用 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"}]

你可能感兴趣的:(RPC,protobuf)