本文以一个简单的CURD服务为例演示了如果一步步使用grpc的接口.

使用protobuf

编写proto文件

A.protogithub.com/someproject/api/Apple.protogo_package-I-Iimport

我们先只用protobuf, 因此只需要不需要声明service, 只需要定义message就够了.

syntax = "proto3";
package api;
option go_package="github.com/violin0622/grpc-apple/api";

message Apple{
  int32 number = 1;
  string name = 2;
  Size size = 3;

  enum Size{
    SIZE_UNDEFINED = 0;
    BIG = 4;
    MID = 5;
    SMALL = 6;
  }
}

下载protoc工具

protobuf-3.13.0-osx-x86_64.tar.gz

下载protoc插件: protoc-gen-go

protoc-gen-gogithub.com/golang/protobuf/proto
git clone -b v1.31 git@github.com:protocolbuffers/protobuf-go.git
cd protobuf-go
# go install 会将项目编译生产的二进制文件放入 $GOPATH/bin. 
# 需要把 $GOPATH/bin 加入 $PATH 以使protoc能够找到. 
go install .

编译

编译使用的命令参数可以见另一篇文章.

protoc \
  --go_out=paths=source_relative:. \
  api/apple.proto
apple.pb.go
├── README.md
├── api
│   ├── apple.pb.go
│   └── apple.proto
├── go.mod
└── main.go

使用

google.golang.org/protobuf/protoMarshalUnmashal
package main
import(
	"bufio"
	"fmt"
	"io"
	"io/ioutil"
	"log"
	"os"
	"strings"

  `google.golang.org/protobuf/proto`

  `github.com/grpc-apple/api`
)

func main(){
	if len(os.Args) != 2 {
		log.Fatalf("Usage:  %s ADDRESS_BOOK_FILE\n", os.Args[0])
	}
	fname := os.Args[1]

	// Read the existing address book.
	in, err := ioutil.ReadFile(fname)
	if err != nil {
		if os.IsNotExist(err) {
			fmt.Printf("%s: File not found.  Creating new file.\n", fname)
		} else {
			log.Fatalln("Error reading file:", err)
		}
	}

	// [START marshal_proto]
	book := &pb.AddressBook{}
	// [START_EXCLUDE]
	if err := proto.Unmarshal(in, book); err != nil {
		log.Fatalln("Failed to parse address book:", err)
	}

	// Add an address.
	addr, err := promptForAddress(os.Stdin)
	if err != nil {
		log.Fatalln("Error with address:", err)
	}
	book.People = append(book.People, addr)
	// [END_EXCLUDE]

	// Write the new address book back to disk.
	out, err := proto.Marshal(book)
	if err != nil {
		log.Fatalln("Failed to encode address book:", err)
	}
	if err := ioutil.WriteFile(fname, out, 0644); err != nil {
		log.Fatalln("Failed to write address book:", err)
	}
	// [END marshal_proto]
}

使用grpc

我们需要查询, 创建, 更新, 修改, 删除五个接口.

编写proto文件

api/operation/operations.protoapple.proto
syntax = "proto3";
package operation;
option go_package="github.com/violin0622/grpc-apple/api/operation";
// 引入protobuf官方提供的Empty结构作为部分接口的空返回值. 
import "google/protobuf/empty.proto";
// 引入protobuf官方提供的Field_Mask用于支持修改操作. 
import "google/protobuf/field_mask.proto";
// 引入上级目录的apple文件以复用定义
import "api/apple.proto";

service AppleService{
  rpc DescribeApple(DescribeAppleRequest) returns (api.Apple) {}
  rpc CreateApple(CreateAppleRequest) returns (api.Apple) {}
  rpc UpdateApple(UpdateAppleRequest) returns (api.Apple) {}
  rpc ModifyApple(ModifyAppleRequest) returns (api.Apple) {}
  rpc DestroyApple(DestroyAppleRequest) returns (google.protobuf.Empty) {}
}

// 为了提供调用接口, 我们新声明了五个消息类型, 需要定义. 
message DescribeAppleRequest{
  int32 number = 1;
}
message CreateAppleRequest{
  string name = 2;
  api.Apple.Size size = 3;
}
// 更新操作: 必须指定对象全部的属性, 
// 对于未指定的属性, 应该将其设定为空或默认值; 
message UpdateAppleRequest{
  int32 number = 1;
  string name = 2;
  api.Apple.Size size = 3;
}
// 修改操作: 只需要设定对象需要变更的属性, 
// 对于未指定的属性, 会保留原来的值. 
// grpc中为了支持修改操作, 需要添加额外的FieldMask字段. 
// 不过好在该字段的值不需要用户设定, grpc会自动生成. 
message ModifyAppleRequest{
  int32 number = 1;
  string name = 2;
  api.Apple.Size size = 3;
  google.protobuf.FieldMask mask = 4;
}

message DestroyAppleRequest{
  string name = 1;
}

下载protoc插件: protoc-gen-go-grpc

github.com/golang/protobufgolang.google.org/grpc-go
# 直接安装:
go get -u google.golang.org/grpc

# 或者这样:
git clone git@github.com:grpc/grpc-go.git
cd grpc-go && go install .

编译

由于存在新旧两种插件, 因此编译命令也有了两种.
值得一提的是, protoc 不支持一次性编译多个包, 如果指定了多个包, 会造成错误.
旧版命令:

protoc \
    --go_out=plugins=grpc,paths=source_relative:. \
    api/apple.proto 

protoc \
  -Iapi
  --go_out=plugins=grpc,paths=source_relative:. \
  api/operation/operations.proto

新版命令:

protoc \
  --go_out=paths=source_relative:. \
  --go-grpc_out=paths=source_relative:. \
  api/apple.proto

protoc \
  --go_out=paths=source_relative:. \
  --go-grpc_out=paths=source_relative:. \
  api/operation/operations.proto

显然新版命令比旧版的更长了 -,-!

apple.pb.gooperations.pb.gooperations_grpc.pb.go
├── README.md
├── api
│   ├── apple.pb.go
│   ├── apple.proto
│   └── operation
│       ├── operations.pb.go
│       ├── operations.proto
│       └── operations_grpc.pb.go
├── go.mod
└── main.go

实现接口

新建一个service 包, 将几个接口的具体实现放在里面:
service/service.go:

package service

import (
	"context"
	"log"

	"github.com/golang/protobuf/ptypes/empty"

	. "github.com/violin0622/grpc-apple/api/operation"
	. "github.com/violin0622/grpc-apple/api"
)

// protoc-gen-go-grpc v1.20 之后, 实现服务的时候必须嵌入 UnimplementedAppleServiceServer 以保证能够向后兼容.
type AppleService struct {
	UnimplementedAppleServiceServer
}

func (*AppleService) DescribeApple(ctx context.Context, req *DescribeAppleRequest) (*Apple, error) {
	return &Apple{}, nil
}
func (*AppleService) CreateApple(ctx context.Context, req *CreateAppleRequest) (*Apple, error) {
	log.Println(req)
	return &Apple{}, nil
}
func (*AppleService) UpdateApple(ctx context.Context, req *UpdateAppleRequest) (*Apple, error) {
	return &Apple{}, nil
}
func (*AppleService) ModifyApple(ctx context.Context, req *ModifyAppleRequest) (*Apple, error) {
	return &Apple{}, nil
}
func (*AppleService) DestroyApple(ctx context.Context, req *DestroyAppleRequest) (*empty.Empty, error) {
	return &empty.Empty{}, nil
}

使用

simple-grpc
package main

import (
	"context"
	"log"
	"net"

	"github.com/golang/protobuf/ptypes/empty"
	"google.golang.org/grpc"
	"google.golang.org/grpc/reflection"

	"github.com/violin0622/grpc-apple/api"
	. "github.com/violin0622/grpc-apple/api/operation"
)


func main() {
	grpcServer := grpc.NewServer()
	RegisterAppleServiceServer(grpcServer, &AppleService{})
	reflection.Register(grpcServer)

	if l, err := net.Listen(`tcp`, `:9000`); err != nil {
		log.Fatal(`cannot listen to port 9000: `, err)
	} else if err = grpcServer.Serve(l); err != nil {
		log.Fatal(`cannot start service:`, err)
	}
}

使用grpc-gateway

grpc-gateway 是grpc-ecosystem 的子项目, 用于提供grpc的反向代理, 实现开发GRPC, 提供RestAPI的目的.
grpc-gateway 同样也是 protoc 的插件, 并且仅支持生成Golang语言的桩代码.
但是, 生成的反向代理可以作为独立的进程, 因此实际可以支持各种语言的grpc服务. 只不过作为Golang语言可以实现更多的特性, 比如复用端口同时提供grpc和http两种接口.

修改operations.proto

为了使用grpc-gateway, 需要在 operations.proto 中引入grpc-gateway的proto文件, 并在每个rpc中添加配置.

syntax = "proto3";
package operation;
option go_package="github.com/violin0622/grpc-apple/operation/api";
// 引入protobuf官方提供的Empty结构作为部分接口的空返回值. 
import "google/protobuf/empty.proto";
// 引入protobuf官方提供的Field_Mask用于支持修改操作. 
import "google/protobuf/field_mask.proto";
// 引入annotation用于定义gateway
import "google/api/annotations.proto";
// 引入上级目录的apple文件以复用定义
import "apple.proto";

service AppleService{
  rpc DescribeApple(DescribeAppleRequest) returns (Apple) {
    option (google.api.http).get = "/apples/{number}";
  }
  rpc CreateApple(CreateAppleRequest) returns (Apple) {
    option (google.api.http) = {
      post: "/apples"
      body: "*"
    };
  }
  rpc UpdateApple(UpdateAppleRequest) returns (Apple) {
    option (google.api.http) = {
      put: "/apples/{number}"
      body: "*"
    };
  }
  rpc ModifyApple(ModifyAppleRequest) returns (Apple) {
    option (google.api.http) = {
      patch: "/apples/{number}"
      body: "*"
    };
  }
  rpc DestroyApple(DestroyAppleRequest) returns (google.protobuf.Empty) {
    option (google.api.http).delete = "/apples/{number}";
  }
}

// 为了提供调用接口, 我们新声明了五个消息类型, 需要定义. 
message DescribeAppleRequest{
  int32 number = 1;
}
message CreateAppleRequest{
  string name = 2;
  Apple.Size size = 3;
}
// 更新操作: 必须指定对象全部的属性, 
// 对于未指定的属性, 应该将其设定为空或默认值; 
message UpdateAppleRequest{
  int32 number = 1;
  string name = 2;
  Apple.Size size = 3;
}
// 修改操作: 只需要设定对象需要变更的属性, 
// 对于未指定的属性, 会保留原来的值. 
// grpc中为了支持修改操作, 需要添加额外的FieldMask字段. 
// 不过好在该字段的值不需要用户设定, grpc会自动生成. 
message ModifyAppleRequest{
  int32 number = 1;
  string name = 2;
  Apple.Size size = 3;
  google.protobuf.FieldMask mask = 4;
}

message DestroyAppleRequest{
  int32 number = 1;
}

下载 protoc-gen-grpc-gateway

go get github.com/grpc-ecosystem/grpc-gateway

编译

apple.proto 我们已经编译过了并且没有修改过, 因此可以直接编译 operations.proto

protoc \
    -I. \
    -I$GOPATH/pkg/mod/github.com/grpc-ecosystem/grpc-gateway@v1.14.8/third_party/googleapis  \
    --go_out=paths=source_relative:. \
    --go-grpc_out=paths=source_relative:. \
    --grpc-gateway_out=paths=source_relative:. \
    api/operation/*.proto

使用

go
package main

import (
	"context"
	"log"
	"net"
	"net/http"

	"github.com/golang/protobuf/ptypes/empty"
	"github.com/grpc-ecosystem/grpc-gateway/runtime"
	"google.golang.org/grpc"
	"google.golang.org/grpc/reflection"

	. "github.com/violin0622/grpc-apple/api/operation"
	. "github.com/violin0622/grpc-apple/api"
)

// protoc-gen-go-grpc v1.20 之后, 实现服务的时候必须嵌入 UnimplementedAppleServiceServer 以保证能够向后兼容.
type AppleService struct {
	UnimplementedAppleServiceServer
}

func (*AppleService) DescribeApple(ctx context.Context, req *DescribeAppleRequest) (*Apple, error) {
	return &Apple{}, nil
}
func (*AppleService) CreateApple(ctx context.Context, req *CreateAppleRequest) (*Apple, error) {
	log.Println(req)
	return &Apple{}, nil
}
func (*AppleService) UpdateApple(ctx context.Context, req *UpdateAppleRequest) (*Apple, error) {
	return &Apple{}, nil
}
func (*AppleService) ModifyApple(ctx context.Context, req *ModifyAppleRequest) (*Apple, error) {
	return &Apple{}, nil
}
func (*AppleService) DestroyApple(ctx context.Context, req *DestroyAppleRequest) (*empty.Empty, error) {
	return &empty.Empty{}, nil
}

func main() {
	grpcServer := grpc.NewServer()
	RegisterAppleServiceServer(grpcServer, &AppleService{})
	reflection.Register(grpcServer)
	l, _ := net.Listen(`tcp`, `:8000`)
	go grpcServer.Serve(l)

	httpServer := runtime.NewServeMux()
	RegisterAppleServiceHandlerFromEndpoint(
		context.Background(),
		httpServer,
		`:8000`,
		[]grpc.DialOption{grpc.WithInsecure()},
	)

	if err := http.ListenAndServe(`:9000`, httpServer); err != nil {
		log.Fatal(`cannot start service: `, err)
	}
}

gateway 与 grpc 使用相同的端口(使用TLS)

基本思路是通过判断 Content-Type 字段来分辨入请求是基于HTTP还是GRPC, 然后分别转发到对应的server handler上.
代码位于 reuse-port-tls/server.go

package main

import (
	"context"
	"log"
	"net/http"
	"strings"

	"github.com/grpc-ecosystem/grpc-gateway/runtime"
	"google.golang.org/grpc"
	"google.golang.org/grpc/credentials"
	"google.golang.org/grpc/reflection"

	. "github.com/violin0622/grpc-apple/api/operation"
	"github.com/violin0622/grpc-apple/service"
)

func main() {
	serverCred, err := credentials.NewServerTLSFromFile(`./server.pem`, `./server.key`)
	if err != nil {
		log.Fatal(err)
	}
	clientCred, err := credentials.NewClientTLSFromFile(`./server.pem`, `localhost`)
	if err != nil {
		log.Fatal(err)
	}

	grpcServer := grpc.NewServer(grpc.Creds(serverCred))
	RegisterAppleServiceServer(grpcServer, &service.AppleService{})
	reflection.Register(grpcServer)

	httpServer := runtime.NewServeMux()
	RegisterAppleServiceHandlerFromEndpoint(
		context.Background(),
		httpServer,
		`:8000`,
		[]grpc.DialOption{grpc.WithTransportCredentials(clientCred)},
	)

	http.ListenAndServeTLS(`:8000`, `./server.pem`, `./server.key`,
		http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {

			if r.ProtoMajor == 2 &&
				strings.Contains(r.Header.Get(`Content-Type`), `application/grpc`) {
				log.Println(`grpc`)
				grpcServer.ServeHTTP(w, r)
			} else {
				log.Println(`http`)
				httpServer.ServeHTTP(w, r)
			}
		}),
	)
}

gateway 与 grpc 使用相同的端口(不使用TLS)

代码位于 reuse-port-insecure/server.go.

package main

import (
	"context"
	"log"
	"net/http"
	"strings"

	"github.com/grpc-ecosystem/grpc-gateway/runtime"
	"golang.org/x/net/http2"
	"golang.org/x/net/http2/h2c"
	"google.golang.org/grpc"
	"google.golang.org/grpc/reflection"

	. "github.com/violin0622/grpc-apple/api/operation"
	"github.com/violin0622/grpc-apple/service"
)

func main() {
	grpcServer := grpc.NewServer()
	RegisterAppleServiceServer(grpcServer, &service.AppleService{})
	reflection.Register(grpcServer)

	httpServer := runtime.NewServeMux()
	RegisterAppleServiceHandlerFromEndpoint(
		context.Background(),
		httpServer,
		`:8000`,
		[]grpc.DialOption{grpc.WithInsecure()},
	)

	http.ListenAndServe(
		`:8000`,
		h2c.NewHandler(
			http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {

				if r.ProtoMajor == 2 &&
					strings.Contains(r.Header.Get(`Content-Type`), `application/grpc`) {
					log.Println(`grpc`)
					grpcServer.ServeHTTP(w, r)
				} else {
					log.Println(`http`)
					httpServer.ServeHTTP(w, r)
				}
			}),
			&http2.Server{}),
	)
}