0. 前言 1. 搭建环境

1.1 Mac 环境

  • 首先使用 Mac 环境,配置如下:
$ go env        
GOARCH="amd64"    # macOS 环境
GOOS="darwin"    # 在第二节使用 Docker 构建 alpine 镜像时需修改为 linux
GOPATH="/Users/wuyin/Go"
GOROOT="/usr/local/go"
安装 ProtoBuf:
brew install protobuf
  •  依顺序安装 Golang 依赖:
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.
  • 安装 Golang 依赖:
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
  •  定义 protobuf 通信协议文件:
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. 参考文献