0. 前言
1. 搭建环境
1.1 Mac 环境
$ go env
GOARCH="amd64" # macOS 环境
GOOS="darwin" # 在第二节使用 Docker 构建 alpine 镜像时需修改为 linux
GOPATH="/Users/wuyin/Go"
GOROOT="/usr/local/go"
安装 ProtoBuf:
brew install protobuf
git clone https://github.com/grpc/grpc-go.git $GOPATH/src/google.golang.org/grpc
git clone https://github.com/golang/net.git $GOPATH/src/golang.org/x/net
git clone https://github.com/golang/text.git$GOPATH/src/golang.org/x/text
go get -u github.com/golang/protobuf/protoc-gen-go
git clone https://github.com/grpc/grpc-go.git $GOPATH/src/google.golang.org/grpc
git clone https://github.com/google/go-genproto.git $GOPATH/src/google.golang.org/genproto
- 文中使用了 make 工具来高效编译,Mac 环境自带了 make 工具,Windows 环境下需要手动安装
1.2 Windows 环境
- 首先安装 Golang 环境,配置环境变量
- 安装 make 工具:
-
- 勾选 mingw32-gcc-g++,并安装(部分包安装失败没有关系):
-
- 点击 All Packages,勾选 mingw32-make:
-
- 打开 MinGW 的安装目录的 bin文件夹,将 mingw32-make.exe 重命名为 make.exe
- 添加 bin 目录到环境变量
- 执行 make 查看安装:
$ make
make: *** No targets specified and no makefile found. Stop.
- 安装 ProtoBuf:
- 解压出 protoc.exe 的 bin 目录(全英文),加入环境变量
- 执行 protoc 查看安装:
$ protoc
Usage: D:\My_Code\ProtoBuf\protoc-3.7.1-win64\bin\protoc.exe [OPTION] PROTO_FILES
Parse PROTO_FILES and generate output based on the options given:
-IPATH, --proto_path=PATH Specify the directory in which to search for
imports. May be specified multiple times;
directories will be searched in order. If not
given, the current working directory is used.
If not found in any of the these directories,
the --descriptor_set_in descriptors will be
checked for required proto file.
--version Show version info and exit.
-h, --help Show this text and exit.
--encode=MESSAGE_TYPE Read a text-format message of the given type
from standard input and write it in binary
to standard output. The message type must
be defined in PROTO_FILES or their imports.
--decode=MESSAGE_TYPE Read a binary message of the given type from
standard input and write it in text format
to standard output. The message type must
be defined in PROTO_FILES or their imports.
--decode_raw Read an arbitrary protocol message from
standard input and write the raw tag/value
pairs in text format to standard output. No
PROTO_FILES should be given when using this
flag.
--descriptor_set_in=FILES Specifies a delimited list of FILES
each containing a FileDescriptorSet (a
protocol buffer defined in descriptor.proto).
The FileDescriptor for each of the PROTO_FILES
provided will be loaded from these
FileDescriptorSets. If a FileDescriptor
appears multiple times, the first occurrence
will be used.
-oFILE, Writes a FileDescriptorSet (a protocol buffer,
--descriptor_set_out=FILE defined in descriptor.proto) containing all of
the input files to FILE.
--include_imports When using --descriptor_set_out, also include
all dependencies of the input files in the
set, so that the set is self-contained.
--include_source_info When using --descriptor_set_out, do not strip
SourceCodeInfo from the FileDescriptorProto.
This results in vastly larger descriptors that
include information about the original
location of each decl in the source file as
well as surrounding comments.
--dependency_out=FILE Write a dependency output file in the format
expected by make. This writes the transitive
set of input file paths to FILE
--error_format=FORMAT Set the format in which to print errors.
FORMAT may be 'gcc' (the default) or 'msvs'
(Microsoft Visual Studio format).
--print_free_field_numbers Print the free field numbers of the messages
defined in the given proto files. Groups share
the same field number space with the parent
message. Extension ranges are counted as
occupied fields numbers.
--plugin=EXECUTABLE Specifies a plugin executable to use.
Normally, protoc searches the PATH for
plugins, but you may specify additional
executables not in the path using this flag.
Additionally, EXECUTABLE may be of the form
NAME=PATH, in which case the given plugin name
is mapped to the given executable even if
the executable's own name differs.
--cpp_out=OUT_DIR Generate C++ header and source.
--csharp_out=OUT_DIR Generate C# source file.
--java_out=OUT_DIR Generate Java source file.
--js_out=OUT_DIR Generate JavaScript source.
--objc_out=OUT_DIR Generate Objective C header and source.
--php_out=OUT_DIR Generate PHP source file.
--python_out=OUT_DIR Generate Python source file.
--ruby_out=OUT_DIR Generate Ruby source file.
@<filename> Read options and filenames from file. If a
relative file path is specified, the file
will be searched in the working directory.
The --proto_path option will not affect how
this argument file is searched. Content of
the file will be expanded in the position of
@<filename> as in the argument list. Note
that shell expansion is not applied to the
content of the file (i.e., you cannot use
quotes, wildcards, escapes, commands, etc.).
Each line corresponds to a single argument,
even if it contains spaces.
git clone https://github.com/grpc/grpc-go.git $GOPATH/src/google.golang.org/grpc
git clone https://github.com/golang/net.git $GOPATH/src/golang.org/x/net
git clone https://github.com/golang/text.git$GOPATH/src/golang.org/x/text
go get -u github.com/golang/protobuf/protoc-gen-go
git clone https://github.com/grpc/grpc-go.git $GOPATH/src/google.golang.org/grpc
git clone https://github.com/google/go-genproto.git $GOPATH/src/google.golang.org/genproto
- 注意:protoc-gen-go 安装后,要将 $GOPATH/bin 里的 protoc-gen-go.exe 拷贝至 $GOROOT/bin 中才可以生效
consignment-service 微服务开发
- 项目命名为 shippy:
- 在 $GOPATH 的 src 目录下新建 shippy 项目目录
- 在项目目录下新建文件 consignment-service/proto/consignment/consignment.proto
$GOPATH/src
└── shippy
└── consignment-service
└── proto
└── consignment
└── consignment.proto
syntax = "proto3";
package go.micro.srv.consignment;
// 货轮微服务
service ShippingService {
// 托运一批货物
rpc CreateConsignment (Consignment) returns (Response) {
}
}
// 货轮承运的一批货物
message Consignment {
string id = 1; // 货物编号
string description = 2; // 货物描述
int32 weight = 3; // 货物重量
repeated Container containers = 4; // 这批货有哪些集装箱
string vessel_id = 5; // 承运的货轮
}
// 单个集装箱
message Container {
string id = 1; // 集装箱编号
string customer_id = 2; // 集装箱所属客户的编号
string origin = 3; // 出发地
string user_id = 4; // 集装箱所属用户的编号
}
// 托运结果
message Response {
bool created = 1; // 托运成功
Consignment consignment = 2;// 新托运的货物
}
- 新建 consignment-service/Makefile:
build:
# 一定要注意 Makefile 中的缩进,否则执行 make build 可能报错:make: Nothing to be done for `build'.
# protoc 命令前边是一个 Tab,不是四个空格
protoc -I. --go_out=plugins=grpc:$(GOPATH)/src/shippy/consignment-service proto/consignment/consignment.proto
- 执行 make build,在 proto/consignment 目录下生成 consignment.pb.go
- consignment.proto 与 consignment.pb.go 的对应关系:
- service:定义了微服务 ShippingService 要暴露为外界调用的函数:CreateConsignment,由 protobuf 编译器的 grpc 插件处理后生成 interface
- message:定义了通信的数据格式,由 protobuf 编译器处理后生成 struct
- 创建 consignment-service/main.go 实现服务端:
package main
import (
// 导如 protoc 自动生成的包
pb "shippy/consignment-service/proto/consignment"
"context"
"net"
"log"
"google.golang.org/grpc"
)
const (
PORT = ":50051"
)
//
// 仓库接口
//
type IRepository interface {
Create(consignment *pb.Consignment) (*pb.Consignment, error) // 存放新货物
}
//
// 我们存放多批货物的仓库,实现了 IRepository 接口
//
type Repository struct {
consignments []*pb.Consignment
}
func (repo *Repository) Create(consignment *pb.Consignment) (*pb.Consignment, error) {
repo.consignments = append(repo.consignments, consignment)
return consignment, nil
}
func (repo *Repository) GetAll() []*pb.Consignment {
return repo.consignments
}
//
// 定义微服务
//
type service struct {
repo Repository
}
//
// service 实现 consignment.pb.go 中的 ShippingServiceServer 接口
// 使 service 作为 gRPC 的服务端
//
// 托运新的货物
func (s *service) CreateConsignment(ctx context.Context, req *pb.Consignment) (*pb.Response, error) {
// 接收承运的货物
consignment, err := s.repo.Create(req)
if err != nil {
return nil, err
}
resp := &pb.Response{Created: true, Consignment: consignment}
return resp, nil
}
func main() {
listener, err := net.Listen("tcp", PORT)
if err != nil {
log.Fatalf("failed to listen: %v", err)
}
log.Printf("listen on: %s\n", PORT)
server := grpc.NewServer()
repo := Repository{}
// 向 rRPC 服务器注册微服务
// 此时会把我们自己实现的微服务 service 与协议中的 ShippingServiceServer 绑定
pb.RegisterShippingServiceServer(server, &service{repo})
if err := server.Serve(listener); err != nil {
log.Fatalf("failed to serve: %v", err)
}
}
- 创建 consingment-cli/cli.go 实现客户端:
package main
import (
pb "shippy/consignment-service/proto/consignment"
"io/ioutil"
"encoding/json"
"errors"
"google.golang.org/grpc"
"log"
"os"
"context"
)
const (
ADDRESS = "localhost:50051"
DEFAULT_INFO_FILE = "consignment.json"
)
// 读取 consignment.json 中记录的货物信息
func parseFile(fileName string) (*pb.Consignment, error) {
data, err := ioutil.ReadFile(fileName)
if err != nil {
return nil, err
}
var consignment *pb.Consignment
err = json.Unmarshal(data, &consignment)
if err != nil {
return nil, errors.New("consignment.json file content error")
}
return consignment, nil
}
func main() {
// 连接到 gRPC 服务器
conn, err := grpc.Dial(ADDRESS, grpc.WithInsecure())
if err != nil {
log.Fatalf("connect error: %v", err)
}
defer conn.Close()
// 初始化 gRPC 客户端
client := pb.NewShippingServiceClient(conn)
// 在命令行中指定新的货物信息 json 文件
infoFile := DEFAULT_INFO_FILE
if len(os.Args) > 1 {
infoFile = os.Args[1]
}
// 解析货物信息
consignment, err := parseFile(infoFile)
if err != nil {
log.Fatalf("parse info file error: %v", err)
}
// 调用 RPC
// 将货物存储到我们自己的仓库里
resp, err := client.CreateConsignment(context.Background(), consignment)
if err != nil {
log.Fatalf("create consignment error: %v", err)
}
// 新货物是否托运成功
log.Printf("created: %t", resp.Created)
}
- 创建 consignment-cli/consignment.json 定义输入文件:
{
"description": "This is a test consignment",
"weight": 550,
"containers": [
{
"customer_id": "cust001",
"user_id": "user001",
"origin": "Manchester, United Kingdom"
}
],
"vessel_id": "vessel001"
}
- 新增一个 RPC 查看所有被托运的货物,加入一个 GetConsignments 方法查看所有存在的 consignment:
syntax = "proto3";
package go.micro.srv.consignment;
// 货轮微服务
service ShippingService {
// 托运一批货物
rpc CreateConsignment (Consignment) returns (Response) {
}
// 查看托运货物信息
rpc GetConsignments (GetRequest) returns (Response) {
}
}
// 货轮承运的一批货物
message Consignment {
string id = 1; // 货物编号
string description = 2; // 货物描述
int32 weight = 3; // 货物重量
repeated Container containers = 4; // 这批货有哪些集装箱
string vessel_id = 5; // 承运的货轮
}
// 单个集装箱
message Container {
string id = 1; // 集装箱编号
string customer_id = 2; // 集装箱所属客户的编号
string origin = 3; // 出发地
string user_id = 4; // 集装箱所属用户的编号
}
// 托运结果
message Response {
bool created = 1; // 托运成功
Consignment consignment = 2; // 新托运的货物
repeated Consignment consignments = 3; // 目前所有托运的货物
}
// 查看货物信息的请求
// 客户端想要从服务端请求数据,必须有请求格式,那篇为空
message GetRequest {
}
- 执行 make build 更新 consignment.pb.go
- 新增方法后需要更新 consignment-service/main.go,否则会报错:
package main
import (
// 导入 protoc 生成的包
"context"
"google.golang.org/grpc"
"log"
"net"
pb "shippy/consignment-service/proto/consignment"
)
const (
PORT = ":50051"
)
//
// 仓库接口
//
type IRespository interface {
Create(consignment *pb.Consignment) (*pb.Consignment, error) // 存放新货物
GetAll() []*pb.Consignment // 获取仓库中所有的货物
}
//
// 我们存放多批货物的仓库,实现了 IRepository 接口
//
type Respository struct {
consignments []*pb.Consignment
}
func (repo *Respository) Create(consignment *pb.Consignment) (*pb.Consignment, error) {
repo.consignments = append(repo.consignments, consignment)
return consignment, nil
}
func (repo *Respository) GetAll() []*pb.Consignment {
return repo.consignments
}
//
// 定义微服务
//
type Service struct {
repo Respository
}
func (service *Service) CreateConsignment(ctx context.Context, req *pb.Consignment) (*pb.Response, error) {
consignment, err := service.repo.Create(req)
if err != nil {
return nil, err
}
resp := &pb.Response{
Created: true,
Consignment: consignment,
}
log.Printf("create consignment: %+v", resp)
return resp, err
}
func (service *Service) GetConsignments(ctx context.Context, req *pb.GetRequest) (*pb.Response, error) {
allConsignments := service.repo.GetAll()
resp := &pb.Response{Consignments: allConsignments}
return resp, nil
}
func main() {
listener, err := net.Listen("tcp", PORT)
if err != nil {
log.Fatalf("failed to listen: %v", PORT)
}
log.Printf("listen on: %v", PORT)
server := grpc.NewServer()
repo := Respository{}
pb.RegisterShippingServiceServer(server, &Service{
repo: repo,
})
if err = server.Serve(listener) ; err != nil {
log.Fatalf("failed to serve: %v", err)
}
}
- 更新 consignment-cli/cli.go,测试 GetConsignments 方法:
package main
import (
"context"
"encoding/json"
"google.golang.org/grpc"
"io/ioutil"
"log"
"os"
pb "shippy/consignment-service/proto/consignment"
)
const (
ADDRESS = ":50051"
DEFAULT_INFO_FILE = "D:\\My_Code\\Golang\\src\\shippy\\consignment-cli\\consignment.json"
)
func parseFile(fileName string) (*pb.Consignment, error) {
data, err := ioutil.ReadFile(fileName)
if err != nil {
return nil, err
}
var consignment *pb.Consignment
if err = json.Unmarshal(data, &consignment) ; err != nil {
return nil, err
} else {
return consignment, nil
}
}
func main() {
conn, err := grpc.Dial(ADDRESS, grpc.WithInsecure())
if err != nil {
log.Fatalf("connect error: %v", err)
}
defer conn.Close()
// 初始化 grpc 客户端
client := pb.NewShippingServiceClient(conn)
infoFile := DEFAULT_INFO_FILE
if len(os.Args) > 1 {
infoFile = os.Args[1]
}
// 解析货物信息
consignment, err := parseFile(infoFile)
if err != nil {
log.Fatalf("parse info file error: %v", err)
}
resp, err := client.CreateConsignment(context.Background(), consignment)
if err != nil {
log.Fatalf("create consignment error: %v", err)
}
// 新货物是否托运成功
log.Printf("created: %t", resp.Created)
// 列出目前所有托运的货物
resp, err = client.GetConsignments(context.Background(), &pb.GetRequest{})
if err != nil {
log.Fatalf("failed to list consignments: %v", err)
}
for _, c := range resp.Consignments {
log.Printf("%+v", c)
}
}
3. 总结
- 至此,初步完成一个几个基于 proto 文件定义通信格式和通信接口,实现一个简单客户端和服务端的过程
- 比较重要的是定义 service 和 message,根据 service 定义分别会生成服务端和客户端 inteface
- 根据 service 定义的 interface 在服务端实现具体的逻辑
- 启动服务端时需要注册 grpc server 和定义的 service 接口实现
- 客户端调用 RPC 接口需要传递 context 和 interface 里定义的方法对应的参数
4. 参考文献