前言


logo logo

gRPC 是一个高性能、开源、通用的 RPC 框架,由 Google 推出,基于 HTTP2 协议标准设计开发,采用 Protocol Buffers 数据序列化协议,支持多种开发语言,广泛应用于分布式系统服务间的调用。

本项目旨在通过一些简单的示例,提供一个简明的 go gRPC 使用指南,帮助新手快速学会使用 gRPC,而非深究其底层原理,内容包含 gRPC 常用的功能特性,以及 gRPC 生态中的一些工具。

本项目及源码地址:go-grpc-tutorial

入门


这个部分通过创建一个简单的服务说明 go gRPC 的基本使用方法和场景,并介绍 protobuf 的基本语法。

环境准备

protobuf 编译器

项目地址:google/protobuf

这里使用brew工具安装,也可以下载编译好的可执行文件:

$ brew install protobuf

执行protoc命令查看当前版本:

$ protoc --version
libprotoc 3.21.6

go 编译插件

项目地址:

安装:

$ go install google.golang.org/protobuf/cmd/protoc-gen-go@v1.28
$ go install google.golang.org/grpc/cmd/protoc-gen-go-grpc@v1.2

$ export PATH="$PATH:$(go env GOPATH)/bin"

Hello gRPC


从一个简单的示例说明 go gRPC 的基本使用流程,实现一个 PingPong 服务,客户端发送 ping 请求,服务端返回 pong 响应。

源码目录:

|—- src/
	|-- ping/
		|—— client.go // 客户端
		|—— server.go // 服务端
	|—- protos/ping/
		|—— ping.proto   // protobuf描述文件
		|—— ping.pb.go   // protoc编译生成
    	|-- ping_grpc.pb.go // protoc编译生成

编写 protobuf 文件

// src/protos/ping/ping.proto
syntax = "proto3"; // 指定proto版本
package protos;     // 指定包名

// 指定go包路径
option go_package = "protos/ping";

// 定义PingPong服务
service PingPong {
    // Ping 发送 ping 请求,接收 pong 响应
    rpc Ping(PingRequest) returns (PongResponse);
}

// PingRequest 请求结构
message PingRequest {
    string value = 1; // value字段为string类型
}

// PongResponse 响应结构
message PongResponse {
    string value = 1; // value字段为string类型
}

定义了一个名为 PingPong 的 service,包含一个 Ping 方法,同时声明了 PingRequestPongResponse 消息结构用于请求和响应。客户端使用 PingRequest 参数调用 Ping 方法请求服务端,服务端响应 PongResponse 消息,一个基本的服务就定义好了。

编译 protobuf 文件

$ cd src
$ protoc --go_out=. --go-grpc_out=. ./protos/ping/ping.proto

在src目录执行编译命令,会在目录 src/protos/ping 内生成两个文件 ping.pb.goping_grpc.pb.go。可以大概看一下这两个文件的内容,ping.pb.go 包含了之前定义的两个message相关的结构,ping_grpc.pb.go 包含了定义的service相关的客户端和服务端接口,不要修改这两个文件的内容

实现服务端接口

// src/ping/server.go
package main

import (
	"context"
	"log"
	"net"

	"google.golang.org/grpc"

	pb "github.com/jergoo/go-grpc-tutorial/protos/ping" // 引入编译生成的包
)

// PingPongServer 实现 pb.PingPongServer 接口
type PingPongServer struct {
	pb.UnimplementedPingPongServer
}

// Ping 单次请求-响应模式
func (s *PingPongServer) Ping(ctx context.Context, req *pb.PingRequest) (*pb.PongResponse, error) {
	return &pb.PongResponse{Value: "pong"}, nil
}

// 启动server
func main() {
	srv := grpc.NewServer()
	// 注册 PingPongServer
	pb.RegisterPingPongServer(srv, &PingPongServer{})
	lis, err := net.Listen("tcp", ":1234")
	if err != nil {
		log.Fatal(err)
	}
	log.Println("listen on 1234")
	srv.Serve(lis)
}

服务端引入编译生成的包,定义一个 PingPongServer 用于实现约定的接口,接口描述可以查看 ping_grpc.pb.go 文件中的 PingPongServer 接口。实例化 grpc Server 并注册 PingPongServer 开始提供服务。

客户端调用

// src/ping/client.go
package main

import (
	"context"
	"log"

	"google.golang.org/grpc"

	pb "github.com/jergoo/go-grpc-tutorial/protos/ping" // 引入编译生成的包
)

// Ping 单次请求-响应模式
func Ping() {
	conn, err := grpc.Dial("localhost:1234", grpc.WithInsecure())
	if err != nil {
		log.Fatal(err)
	}
	defer conn.Close()

	// 实例化客户端并调用
	client := pb.NewPingPongClient(conn)
	res, err := client.Ping(context.Background(), &pb.PingRequest{Value: "ping"})
	if err != nil {
		log.Fatal(err)
	}
	log.Println(res.Value)
}

客户端初始化连接,使用 ping_grpc.pb.go 中的 PingPongClient 实例调用 Ping 方法,即可向服务端发起请求并获取响应,就像调用本地方法一样。


以上就是一个最基础的 gRPC 服务,使用非常简单,底层网络细节全部由 gRPC 处理,开发者只需要关注业务接口设计和实现,基本流程如下:

  1. 编写 protobuf 描述文件,定义消息结构和服务接口
  2. 编译 protobuf 文件,生成服务端和客户端接口代码
  3. 实现 *_grpc.pb.go 文件中描述的服务端接口
  4. 使用 *_grpc.pb.go 文件中的client调用服务

gRPC 流


从其名称可以理解,流就是持续不断的传输。有一些业务场景请求或者响应的数据量比较大,不适合使用普通的 RPC 调用通过一次请求-响应处理,一方面是考虑数据量大对请求响应时间的影响,另一方面业务场景的设计不一定需要一次性处理完所有数据,这时就可以使用流来分批次传输数据。gRPC支持单向流和双向流,只需要在 service 的 rpc 方法描述中通过 stream 关键字指定启用流特性就好了,下面通过两个示例来说明使用方法。

源码目录:

|—- src/
	|-- ping/
		|—— client.go // 客户端
		|—— server.go // 服务端
	|—- protos/ping/
		|—— ping.proto   // protobuf描述文件
		|—— ping.pb.go   // protoc编译生成
    	|-- ping_grpc.pb.go // protoc编译生成

单向流

单向流是指客户端和服务端只有一端开启流特性,这里的单向特指发送数据的方向

  • 当服务端开启流时,客户端和普通 RPC 调用一样通过一次请求发送数据,服务端通过流分批次响应。
  • 当客户端开启流时,客户端通过流分批次发送请求数据,服务端接完所有数据后统一响应一次。

服务端流

定义一个 MultiPong 方法,在服务端开启流,功能是接收到客户端的请求后响应10次 pong 消息。

...

service PingPong {
    // 服务端流模式,在响应消息前添加 stream 关键字
    rpc MultiPong(PingRequest) returns (stream PongResponse);
}

...

服务端实现:第二个参数为 stream 对象的引用,可以通过它的 Send 方法发送数据。

// src/ping/server.go

// MultiPong 服务端流模式
func (s *PingPongServer) MultiPong(req *pb.PingRequest, stream pb.PingPong_MultiPongServer) error {
	for i := 0; i < 10; i++ {
		data := &pb.PongResponse{Value: "pong"}
		// 发送消息
		err := stream.Send(data)
		if err != nil {
			return err
		}
	}
	return nil
}

客户端实现:请求方式和普通 RPC 没有区别,重点关注对响应数据流的处理,通过一个 for 循环接收数据直到结束。

// src/ping/client.go

func MultiPong() {

    ...
    
    // 获得对 stream 对象的引用
	stream, err := client.MultiPong(context.Background(), &pb.PingRequest{Value: "ping"})
	if err != nil {
		log.Fatal(err)
	}

	// 循环接收响应数据流
	for {
		msg, err := stream.Recv()
		if err != nil {
            // 数据结束
			if err == io.EOF {
				break
			}
			log.Fatal(err)
		}
		log.Println(msg.Value)
	}
}

客户端流

定义一个 MultiPing 方法,在客户端开启流,功能是持续发送多个 ping 请求,服务端统一响应一次。

...

service PingPong {
	// 客户端流模式,在请求消息前添加 stream 关键字
	rpc MultiPing(stream PingRequest) returns (PongResponse);
}

...

服务端实现:只有一个参数为 stream 对象的引用,可以通过它的 Recv 方法接收数据。使用 SendAndClose 方法关闭流并响应,服务端可以根据需要提前关闭。

// src/ping/server.go

// MultiPing 客户端流模式
func (s *PingPongServer) MultiPing(stream pb.PingPong_MultiPingServer) error {
	msgs := []string{}
	for {
		// 提前结束接收消息
		if len(msgs) > 5 {
			return stream.SendAndClose(&pb.PongResponse{Value: "ping enough, max 5"})
		}

		msg, err := stream.Recv()
		if err != nil {
			// 客户端消息结束,返回响应信息
			if err == io.EOF {
				return stream.SendAndClose(&pb.PongResponse{Value: fmt.Sprintf("got %d ping", len(msgs))})
			}
			return err
		}
		msgs = append(msgs, msg.Value)
	}
}

客户端实现:调用 MultiPing 方法时不再指定请求参数,而是通过返回的 stream 对象的 Send 分批发送数据。

// src/ping/client.go

func MultiPing() {
    ...

    // 调用并得到 stream 对象
    stream, err := client.MultiPing(context.Background())
	if err != nil {
		log.Fatal(err)
	}

	// 发送数据
	for i := 0; i < 6; i++ {
		data := &pb.PingRequest{Value: "ping"}
		err = stream.Send(data)
		if err != nil {
			log.Fatal(err)
		}
	}

	// 发送结束并获取服务端响应
	res, err := stream.CloseAndRecv()
	if err != nil {
		log.Fatal(err)
	}

	log.Println(res.Value)
}

运行结果:

// 发送5个ping
2022/09/27 00:00:00 got 5 ping

// 发送10个ping
2022/09/27 00:00:00 ping enough, max 5

双向流

双向流是指客户端在发送数据和服务端响应数据的过程中都启用流特性,实际上单向流只是双向流的特例,有了上面的基础,双向流就很好理解了。

定义一个 MultiPingPong 方法,在客户端和服务端都开启流,功能是服务端每接收到两个 ping 就响应一次 pong。

...

service PingPong {
	// 双向流模式
	rpc MultiPingPong(stream PingRequest) returns (stream PongResponse);
}

...

服务端实现:同样通过 stream 的 RecvSend 方法接收和发送数据。

// src/ping/server.go

func (s *PingPongServer) MultiPingPong(stream pb.PingPong_MultiPingPongServer) error {
	msgs := []string{}
	for {
		// 接收消息
		msg, err := stream.Recv()
		if err != nil {
			if err == io.EOF {
				break
			}
			return err
		}
		msgs = append(msgs, msg.Value)

		// 每收到两个消息响应一次
		if len(msgs)%2 == 0 {
			err = stream.Send(&pb.PongResponse{Value: "pong"})
			if err != nil {
				return err
			}
		}
	}
	return nil
}

客户端实现:这里在另外一个 goroutine 里处理接收数据的逻辑来演示同时发送和接收数据。

// src/ping/client.go
func MultiPingPong() {
    ...

	stream, err := client.MultiPingPong(context.Background())
	if err != nil {
		log.Fatal(err)
	}

	// 在另一个goroutine中处理接收数据
	c := make(chan struct{})
	go func(stream pb.PingPong_MultiPingPongClient, c chan struct{}) {
        defer func() {
			c <- struct{}{}
		}()
		for {
			msg, err := stream.Recv()
			if err != nil {
				if err == io.EOF {
					break
				}
				log.Fatal(err)
			}
			log.Printf("recv:%s\n", msg.Value)
		}
	}(stream, c)

	// 发送数据
	for i := 0; i < 6; i++ {
		data := &pb.PingRequest{Value: "ping"}
		err = stream.Send(data)
		if err != nil {
			log.Fatal(err)
		}
		log.Printf("send:%s\n", data.Value)

        // 延时一段时间发送,等待响应结果
		time.Sleep(500 * time.Millisecond)
	}

	// 结束发送
	stream.CloseSend()
	// 等待接收完成
	<-c
}

运行结果:

2022/09/29 09:09:59 send:ping
2022/09/29 09:10:00 send:ping
2022/09/29 09:10:00 recv:pong
2022/09/29 09:10:00 send:ping
2022/09/29 09:10:01 send:ping
2022/09/29 09:10:01 recv:pong
2022/09/29 09:10:01 send:ping
2022/09/29 09:10:02 send:ping
2022/09/29 09:10:02 recv:pong

Protobuf


ProtoBuf 是由 Google 开发的一种数据描述语言,应用于跨语言的接口规范描述,同时也是一种数据序列化协议(类似于XML、JSON),能够将数据进行序列化,广泛应用在数据存储、通信协议等方面。这里只介绍基本语法,重点关注如何描述一个服务,更多详细语法及高级特性请查看官方文档

基本结构

一个 protobuf 描述文件以.proto做为文件后缀,基本由三部分构成:

  • 头部区域声明版本、包名、导入包及文件级别的option等信息
  • 定义 service 及其 rpc 方法描述
  • 定义 message/enum 等自义定数据类型

示例:

// -------------------- 头部区域 ----------------------------
syntax = "proto3";                        // 指定proto版本号,最新版使用proto3
import "some other package";              // 导入其它包
package pacakge_name;                     // 指定包名

option go_package = "go package path";    // go package 文件选项

// -------------------- 服务描述 ----------------------------

service ServiceName {
    rpc FuncName(Request) returns (Response);
}

// -------------------- 自定义数据类型 ----------------------------

message Request {
    string value = 1;
}

message Response {
    string value = 1;
}

规范

  • 除结构定义外的语句以分号结尾,结构定义包括:message、service、enum
  • message 命名采用驼峰命名方式,字段命名采用小写字母加下划线分隔方式
  • enums 类型名采用驼峰命名方式,字段命名采用大写字母加下划线分隔方式
  • service 名称与 rpc 方法名统一采用驼峰式命名
  • 支持以 // 开头的单行注释

头部区域

首行要求明确声明使用的 protobuf 版本

syntax = "proto3"; 

导入

可以使用import语句导入使用其它 protobuf 描述文件中声明的类型,protoc 编译器会在编译命令中 -I / --proto_path参数指定的目录中查找导入的文件,如果没有指定该参数,默认在当前目录中查找。

示例:

syntax = "proto3"; 
import "google/protobuf/wrappers.proto"; // 导入其它包

...

service SomeService {
    // 使用包路径引用导入包的类型
    rpc getInfo(google.protobuf.StringValue) returns (Response);
}

包名

.proto文件中使用package声明包名,避免命名冲突。

syntax = "proto3";
package foo.bar;
message Open {...}

在其他的消息格式定义中可以使用包名+消息名的方式来使用类型,如:

message Foo {
    ...
    foo.bar.Open open = 1;
    ...
}

message

一个 message 定义描述了一个消息格式,是一个复合类型,和编程语言的结构体类似,protobuf 内置了一些基本类型,使用基本类型和其它复合类型组合定义一个 message 类型。

  • 字段声明格式:[类型] [字段名] = [Tag];
  • 所有的字段需要前置声明数据类型,除了 protobuf 内置基本类型也可以是其它 message 类型
  • 每个字段都有一个唯一的数值标签,这些标签用于标识该字段在消息中的二进制格式,使用中的类型不应该随意改动
  • 可以针对 message 和字段添加注释,注释内容会同步到编译生成的源码文件中
  • 可以在类型名前使用 repeated 关键词,声明该字段为数组类型

示例:

// SearchRequest 搜索请求
message SearchRequest {
    string keyword = 1;     // 查询关键词
    int32  page_no = 2;     // 页码
    int32  page_size = 3;   // 数量
    repeated int32 arr = 4; // 数组
}

基本类型

source整型浮点布尔字符串字节数组
protobufint32/uint32/int64/uint64float/doubleboolstringbytes
goint32/uint32/int64/uint64float32/float64boolstring[]byte

enum类型

当定义一个 message 时,想要一个字段只能是一个预定义好的值列表内的一个值,就需要用到enum类型了。注意:每个enum定义的第一个元素值必须是0

示例:

message Response {
  string value = 1;
  Status status = 2; // 使用Status类型
}

enum Status {
    OK = 0;
    FAIL = 1;
}

map类型

proto3支持map类型声明: map<key_type, value_type> field_name = N;

  • key_type类型可以是内置的基本类型(除浮点类型和bytes)
  • value_type可以是除map以外的任意类型
  • map字段不支持repeated属性
  • 不要依赖map类型的字段顺序

示例:

message Project {...}
map<string, Project> projects = 1;

service

service 描述一个RPC服务的接口,使用 rpc 关键字描述方法的签名,方法支持单次请求-响应(unary)和 stream 模式。protoc编译器会根据所选择的不同语言生成服务接口代码。生成的接口代码作为客户端与服务端的约定,服务端必须实现定义的所有接口方法,客户端直接调用同名方法向服务端发起请求。

示例:

service ServiceName {
    rpc Single (Request) returns (Response);                  // unary
    rpc ServerStream (Request) returns (stream Response);     // server stream
    rpc ClientStream (stream Request) returns (Response);     // client stream
    rpc BiStream (stream Request) returns (stream Response);  // bidirectional stream
}

编译

通过定义好的 .proto 文件生成各种语言的代码,需要安装编译器 protoc 及对应语言的插件。参考Github项目google/protobuf安装编译器.

示例命令:

protoc --proto_path=IMPORT_PATH --cpp_out=DST_DIR --java_out=DST_DIR --python_out=DST_DIR --go_out=DST_DIR --go-grpc_out=DST_DIR --ruby_out=DST_DIR --javanano_out=DST_DIR --objc_out=DST_DIR --csharp_out=DST_DIR path/to/file.proto

Protobuf⇢Go转换

我们定义一个示例文件对照说明常用结构的 protobuf 到 go 的转换,只说明关键部分代码,详细内容请查看完整文件。

// src/protos/example/example.proto

syntax = "proto3"; // 指定proto版本
package example; // 指定包名
option go_package="protos/example"; // 指定go包路径

// ExampleService 示例
service ExampleService {
    // Single 单次请求响应模式
    rpc Single(Request) returns (Response);
    // ServerStream 服务端流模式
    rpc ServerStream(Request) returns (stream Response);
    // ClientStream 客户端流模式
    rpc ClientStream(stream Request) returns (Response);
    // BiStream 双向流模式
    rpc BiStream(stream Request) returns (stream Response);
}

// Request 请求结构
message Request {
    string value = 1;
}

// Response 响应结构
message Response {
    string valuee = 1;
}

// Msg message 数据类型示例
message Msg {
    int32 i32 = 1;
    int64 i64 = 2;
    float f32 = 3;
    double f64  = 4;
    string str = 5;
    bool boolean = 6;
    bytes byteArr = 7;
    map<string, string> dict = 8;
    Status status = 9;
    EmbMsg embMsg = 10;
    repeated int64 intArr = 11;
}

message EmbMsg {
    string value = 1;
}

// Status 枚举
enum Status {
    OK = 0;
    FAIL = 1;
}

编译:

> cd src
> protoc --go_out=. --go-grpc_out=. ./protos/example/example.proto

package

在proto文件中使用 package 关键字声明包名,默认转换成go中的包名与此一致。这里使用 go_package 选项用于控制编译结果文件的保存路径,这个路径会和编译命令中的--go_out=. 选项的路径拼接。比如这里当前目录是 src, 编译结果输出路径为 ./protos/example

package example; // 指定包名
option go_package="protos/example"; // 指定go包路径

message

protobuf 中的 message 对应 go 中的 struct,全部使用驼峰命名规则,编译结果文件为 {proto file name}.pb.go

// src/protos/example/example.pb.go

// Msg message 数据类型示例
type Msg struct {
	state         protoimpl.MessageState
	sizeCache     protoimpl.SizeCache
	unknownFields protoimpl.UnknownFields

	I32     int32             `protobuf:"varint,1,opt,name=i32,proto3" json:"i32,omitempty"`
	I64     int64             `protobuf:"varint,2,opt,name=i64,proto3" json:"i64,omitempty"`
	F32     float32           `protobuf:"fixed32,3,opt,name=f32,proto3" json:"f32,omitempty"`
	F64     float64           `protobuf:"fixed64,4,opt,name=f64,proto3" json:"f64,omitempty"`
	Str     string            `protobuf:"bytes,5,opt,name=str,proto3" json:"str,omitempty"`
	Boolean bool              `protobuf:"varint,6,opt,name=boolean,proto3" json:"boolean,omitempty"`
	ByteArr []byte            `protobuf:"bytes,7,opt,name=byteArr,proto3" json:"byteArr,omitempty"`
	Dict    map[string]string `protobuf:"bytes,8,rep,name=dict,proto3" json:"dict,omitempty" protobuf_key:"bytes,1,opt,name=key,proto3" protobuf_val:"bytes,2,opt,name=value,proto3"`
	Status  Status            `protobuf:"varint,9,opt,name=status,proto3,enum=example.Status" json:"status,omitempty"`
	EmbMsg  *EmbMsg           `protobuf:"bytes,10,opt,name=embMsg,proto3" json:"embMsg,omitempty"`
	IntArr  []int64           `protobuf:"varint,11,rep,packed,name=intArr,proto3" json:"intArr,omitempty"`
}

type EmbMsg struct {
	state         protoimpl.MessageState
	sizeCache     protoimpl.SizeCache
	unknownFields protoimpl.UnknownFields

	Value string `protobuf:"bytes,1,opt,name=value,proto3" json:"value,omitempty"`
}

除了会生成对应的结构外,还会有些工具方法,如字段的getter:

func (x *Msg) GetStatus() Status {
	if x != nil {
		return x.Status
	}
	return Status_OK
}

枚举类型会生成对应名称的常量,同时会有两个map方便使用:

// Status 枚举
type Status int32

const (
	Status_OK   Status = 0
	Status_FAIL Status = 1
)

// Enum value maps for Status.
var (
	Status_name = map[int32]string{
		0: "OK",
		1: "FAIL",
	}
	Status_value = map[string]int32{
		"OK":   0,
		"FAIL": 1,
	}
)

service

针对 service 的编译是由 protoc-gen-go-grpc 插件参与处理,编译结果文件为 {proto file name}_grpc.pb.go。代码中包含服务端和客户端接口的定义,客户端接口已经自动实现了,直接供客户端使用者调用,服务端接口需要由服务提供方实现。

// 客户端接口
type ExampleServiceClient interface {
	// Single 单次请求响应模式
	Single(ctx context.Context, in *Request, opts ...grpc.CallOption) (*Response, error)
	// ServerStream 服务端流模式
	ServerStream(ctx context.Context, in *Request, opts ...grpc.CallOption) (ExampleService_ServerStreamClient, error)
	// ClientStream 客户端流模式
	ClientStream(ctx context.Context, opts ...grpc.CallOption) (ExampleService_ClientStreamClient, error)
	// BiStream 双向流模式
	BiStream(ctx context.Context, opts ...grpc.CallOption) (ExampleService_BiStreamClient, error)
}

// 服务端接口
type ExampleServiceServer interface {
	// Single 单次请求响应模式
	Single(context.Context, *Request) (*Response, error)
	// ServerStream 服务端流模式
	ServerStream(*Request, ExampleService_ServerStreamServer) error
	// ClientStream 客户端流模式
	ClientStream(ExampleService_ClientStreamServer) error
	// BiStream 双向流模式
	BiStream(ExampleService_BiStreamServer) error
	mustEmbedUnimplementedExampleServiceServer()
}

参考文档

进阶


这个部分会介绍一些 gRPC 的高级功能特性。

拦截器


在应用开发过程中会有这样的需求,就是在请求执行前后做一些通用的处理逻辑,比如记录日志、tracing、身份认证等,在web框架中一般是使用middleware来实现的,gRPC 在客户端和服务端都支持了拦截器功能,用来处理这种业务需求。

下面基于 PingPong 服务做一些扩展来演示拦截器的使用方法。

源码目录:

|—- src/
	|-- interceptor/
		|—— client.go // 客户端
		|—— server.go // 服务端
	|—- protos/ping/
		|—— ping.proto   // protobuf描述文件
		|—— ping.pb.go   // protoc编译生成
    	|-- ping_grpc.pb.go // protoc编译生成

普通拦截器

在客户端和服务端分别实现一个记录请求日志的拦截器,打印请求前后的信息。

服务端

// src/interceptor/server.go

...

// 服务端拦截器 - 记录请求和响应日志
func serverUnaryInterceptor(ctx context.Context, req interface{}, info *grpc.UnaryServerInfo, handler grpc.UnaryHandler) (resp interface{}, err error) {
	// 前置逻辑
	log.Printf("[Server Interceptor] accept request: %s", info.FullMethod)

	// 处理请求
	response, err := handler(ctx, req)

	// 后置逻辑
	log.Printf("[Server Interceptor] response: %s", response)

	return response, err
}

func main() {
	// 以option的方式添加拦截器
	srv := grpc.NewServer(grpc.UnaryInterceptor(serverUnaryInterceptor))

...

客户端

// src/inteceptor/client.go

...

// 客户端拦截器 - 记录请求和响应日志
func clientUnaryInterceptor(ctx context.Context, method string, req, reply interface{}, cc *grpc.ClientConn, invoker grpc.UnaryInvoker, opts ...grpc.CallOption) error {
	// 前置逻辑
	log.Printf("[Client Interceptor] send request: %s", method)

	// 发起请求
	err := invoker(ctx, method, req, reply, cc, opts...)

	// 后置逻辑
	log.Printf("[Client Interceptor] response: %s", reply)

	return err
}

...

func Ping() {
	// 以option方式添加拦截器
	conn, err := grpc.Dial("localhost:1234", grpc.WithInsecure(), grpc.WithUnaryInterceptor(clientUnaryInterceptor))
	if err != nil {
		log.Fatal(err)
	}

...

这里分别定义了 serverUnaryInterceptorclientUnaryInterceptor 拦截器,函数的签名定义在 google.golang.org/grpc 包中,分别为 UnaryServerInterceptorUnaryClientInterceptor, 在前置逻辑位置可以对请求信息做处理,在后置逻辑位置可以对响应信息做处理。在初始化服务端和客户端连接时以option的形式配置就好了,同时也支持配置多个拦截器。

运行结果:

// server
2022/09/27 00:00:00 [Server Interceptor] accept request: /protos.PingPong/Ping
2022/09/27 00:00:00 [Server Interceptor] response: value:"pong"

// client
2022/09/27 00:00:00 [Client Interceptor] send request: /protos.PingPong/Ping
2022/09/27 00:00:00 [Client Interceptor] response: value:"pong"

流拦截器

同样实现一个打印请求和响应日志的拦截器,只是函数签名变成了 grpc.StreamServerInterceptorgrpc.StreamClientInterceptor

服务端

// src/interceptor/server.go

...

// 服务端拦截器 - 记录stream请求和响应日志
func serverStreamInterceptor(srv interface{}, ss grpc.ServerStream, info *grpc.StreamServerInfo, handler grpc.StreamHandler) error {
	// 前置逻辑
	log.Printf("[Server Stream Interceptor] accept request: %s", info.FullMethod)

	// 处理请求
	err := handler(srv, ss)
	return err
}

...

func main() {
	// 以option的方式添加拦截器
	opts := []grpc.ServerOption{
		grpc.UnaryInterceptor(serverUnaryInterceptor),
		grpc.StreamInterceptor(serverStreamInterceptor),
	}
	srv := grpc.NewServer(opts...)

...

以上实现其实和普通拦截器的使用方式没太大区别,但是流的特性在于请求和响应不是一次性处理完成的,而是多次发送和接收数据,所以我们可能需要在发送和接收数据的过程中处理一些公共逻辑,这才是流拦截器特别的地方。我们注意到 handler 方法调用的第二个参数是一个 grpc.ServerStream 接口类型,这个接口类型包含了 SendMsgRecvMsg 方法,所以我们可以使用一个自定义类型实现这个接口,对原对象进行包装重写这两个方法,这样就能达到我们的目的了。

// 服务端拦截器 - 记录stream请求和响应日志
func serverStreamInterceptor(srv interface{}, ss grpc.ServerStream, info *grpc.StreamServerInfo, handler grpc.StreamHandler) error {
	// 前置逻辑
	log.Printf("[Server Stream Interceptor] accept request: %s", info.FullMethod)

	// 处理请求,使用自定义类型包装 ServerStream
	err := handler(srv, &customServerStream{ss})
	return err
}

type customServerStream struct {
	grpc.ServerStream
}

func (s *customServerStream) SendMsg(m interface{}) error {
	log.Printf("[Server Stream Interceptor] send: %T", m)
	return s.ServerStream.SendMsg(m)
}

func (s *customServerStream) RecvMsg(m interface{}) error {
	log.Printf("[Server Stream Interceptor] recv: %T", m)
	return s.ServerStream.RecvMsg(m)
}

客户端

客户端的使用方式和服务端类似,只是对应的数据处理接口类型变成了 grpc.ClientStream

// src/interceptor/client.go

···

	opts := []grpc.DialOption{
		grpc.WithInsecure(),
		grpc.WithStreamInterceptor(clientStreamInterceptor),
	}
	conn, err := grpc.Dial("localhost:1234", opts...)

···

func clientStreamInterceptor(ctx context.Context, desc *grpc.StreamDesc, cc *grpc.ClientConn, method string, streamer grpc.Streamer, opts ...grpc.CallOption) (grpc.ClientStream, error) {
	// 前置逻辑
	log.Printf("[Client Stream Interceptor] send request: %s", method)

	// 请求
	s, err := streamer(ctx, desc, cc, method, opts...)
	if err != nil {
		return nil, err
	}

	// 自定义类型包装 ClientStream
	return &customClientStream{s}, nil
}

type customClientStream struct {
	grpc.ClientStream
}

func (s *customClientStream) SendMsg(m interface{}) error {
	log.Printf("[Client Stream Interceptor] send: %T", m)
	return s.ClientStream.SendMsg(m)
}

func (s *customClientStream) RecvMsg(m interface{}) error {
	log.Printf("[Client Stream Interceptor] recv: %T", m)
	return s.ClientStream.RecvMsg(m)
}

运行结果,以 MultiPingPong 方法为例,最后一次输出的recv是结束消息,err == io.EOF

// server
2022/10/02 10:53:11 [Server Stream Interceptor] accept request: /protos.PingPong/MultiPingPong
2022/10/02 10:53:11 [Server Stream Interceptor] recv: *ping.PingRequest
2022/10/02 10:53:11 [Server Stream Interceptor] recv: *ping.PingRequest
2022/10/02 10:53:11 [Server Stream Interceptor] send: *ping.PongResponse
2022/10/02 10:53:11 [Server Stream Interceptor] recv: *ping.PingRequest

// client
2022/10/02 10:53:11 [Client Stream Interceptor] send request: /protos.PingPong/MultiPingPong
2022/10/02 10:53:11 [Client Stream Interceptor] send: *ping.PingRequest
2022/10/02 10:53:11 send:ping
2022/10/02 10:53:11 [Client Stream Interceptor] send: *ping.PingRequest
2022/10/02 10:53:11 send:ping
2022/10/02 10:53:12 [Client Stream Interceptor] recv: *ping.PongResponse
2022/10/02 10:53:12 recv:pong
2022/10/02 10:53:13 [Client Stream Interceptor] recv: *ping.PongResponse

注意:在自定义的 RecvMsg 方法中,前置位置只能读取消息的类型,无法读取实际数据,因为这个时候接收到的消息还没有解析处理,如果要获取接收消息的实际数据,需要把自定义的处理逻辑放在后置位置,例如:

func (s *customClientStream) RecvMsg(m interface{}) error {
  err := s.ClientStream.RecvMsg(m)
  log.Printf("[Client Stream Interceptor] recv: %v", m)
  return err
}

metadata


服务间使用 Http 相互调用时,经常会设置一些业务自定义 header 如时间戳、trace信息等,gRPC使用 HTTP/2 协议自然也是支持的,gRPC 通过 google.golang.org/grpc/metadata 包内的 MD 类型提供相关的功能接口。

源码目录:

|—- src/
	|-- metadata/
		|—— client.go // 客户端
		|—— server.go // 服务端
	|—- protos/ping/
		|—— ping.proto   // protobuf描述文件
		|—— ping.pb.go   // protoc编译生成
    	|-- ping_grpc.pb.go // protoc编译生成

类型定义

// MD is a mapping from metadata keys to values. Users should use the following
// two convenience functions New and Pairs to generate MD.
type MD map[string][]string

metadata.MD 类型的定义非常简单,可以像一个普通的 map 一样直接操作,同时 metadata 包里封装了很多工具方法供我们使用。

// 使用 New 方法创建
md := metadata.New(map[string]string{"k1":"v1", "k2", "v2"})

// 直接使用 make 创建
md := make(metadata.MD)

// 使用 Pairs 方法创建
md := metadata.Pairs("k1", "v1-1", "k1", "v1-2")

// 一些操作
md.Set("key", "v1", "v2")
md.Append("key", "v3")
md.Delete("key")
vals := md.Get("key")

发送与接收

客户端

客户端请求的 metadata 是通过设置 context 使用的,metadata 包提供了两个 context 相关的方法,设置好 context 后直接在调用 rpc 方法时传入即可,示例代码如下:

md := metadata.New(map[string]string{"k1":"v1", "k2", "v2"})

// 使用 NewOutgoingContext 初始化一个新的 context
ctx := metadata.NewOutgoingContext(context.Background(), md)

// 使用 AppendToOutgoingContext 向 context 追加 metadata
ctx = metadata.AppendToOutgoingContext(ctx, "k3", "v3")

客户端接收响应中的 metadata 需要区分普通 rpc 和 stream rpc,示例如下:

// 普通 rpc,使用 grpc.Header 方法包装为 CallOption
var md metadata.MD
res, err := client.Ping(ctx, &pb.PingRequest{Value: "ping"}, grpc.Header(&md))

// stream rpc
stream, err := client.MultiPong(context.Background(), &pb.PingRequest{Value: "ping"})
if err != nil {
    log.Fatal(err)
}

// 通过 stream 对象的 Header 方法获取
md, err := stream.Header()
if err != nil {
    log.Fatal(err)
}

服务端

对应客户端请求的 metadata 是使用 context 设置的,那么服务端在接收时自然也是从 context 中读取,metadata 包中的 FromIncommingContext 方法就是用来读取 context 中的 metadata数据的,示例如下:

// unary rpc
func (s *PingPongServer) Ping(ctx context.Context, req *pb.PingRequest) (*pb.PongResponse, error) {
	// 读取请求metadata
	md, ok := metadata.FromIncomingContext(ctx)
	if ok {
		log.Printf("Got md: %v", md)
	}

...

// stream rpc
func (s *PingPongServer) MultiPingPong(stream pb.PingPong_MultiPingPongServer) error {
	md, ok := metadata.FromIncomingContext(stream.Context())
	if ok {
		log.Printf("Got md: %v", md)
	}

...

服务端设置响应的 metadata 也非常简单,只需要调用封装好的 SetHeaderSendHeader 方法即可,示例如下:

// unary rpc
func (s *PingPongServer) Ping(ctx context.Context, req *pb.PingRequest) (*pb.PongResponse, error) {
	// 读取请求metadata
	md, ok := metadata.FromIncomingContext(ctx)
	if ok {
		log.Printf("Got md: %v", md)
	}

	// SetHeader设置响应 metadata
	grpc.SetHeader(ctx, metadata.New(map[string]string{"rkey": "rval"}))
    // 注意 SendHeader 只能调用一次
    // grpc.SendHeader(ctx, metadata.New(map[string]string{"rkey": "rval"}))

	....

// stream rpc, 调用 stream 的 SetHeader 方法
func (s *PingPongServer) MultiPong(req *pb.PingRequest, stream pb.PingPong_MultiPongServer) error {
	stream.SetHeader(metadata.New(map[string]string{"rkey": "rval"}))

    // 注意 SendHeader 只能调用一次
    // stream.SendHeader(metadata.New(map[string]string{"rkey": "rval"}))

安全认证


服务开发中需要考虑服务的安全性,如连接是否加密,用户请求是否有权限等,gRPC 支持基于 TLS 的认证保证连接的安全性,通知也支持基于 token 的认证方式,用于对用户做权限认证。

源码目录:

|—- src/
	|-- auth/
		|-- keys/     // 证书目录
		|—— client.go // 客户端
		|—— server.go // 服务端
	|—- protos/ping/
		|—— ping.proto   // protobuf描述文件
		|—— ping.pb.go   // protoc编译生成
    	|-- ping_grpc.pb.go // protoc编译生成

TLS认证

首先需要准备服务端证书,在 keys 目录存放证书文件。

$ cd src/auth/keys
# 生成证书,-config 替换为对应系统的openssl配置文件目录
$ openssl req -newkey rsa:2048 -x509 -nodes -sha256 -days 3650 \
    -keyout server.key -new -out server.crt \
    -subj /CN=grpc.server -reqexts SAN -extensions SAN \
    -config <(cat /System/Library/OpenSSL/openssl.cnf \
        <(printf '[SAN]\nsubjectAltName=DNS:grpc.server'))

生成证书文件后,我们就可以在服务端通过 ServerOption 开启 TLS 了,示例如下:

// src/auth/server.go

func main() {
	creds, err := credentials.NewServerTLSFromFile("keys/server.crt", "keys/server.key")
	if err != nil {
		log.Fatalf("load crt fail:%v", err)
	}
	opts := []grpc.ServerOption{
		grpc.Creds(creds),
	}

	srv := grpc.NewServer(opts...)

...

类似的,在客户端使用 DialOption 开启 TLS,示例如下:

func Ping() {
	// 读取服务端证书,并制定对应服务名
	cred, err := credentials.NewClientTLSFromFile("keys/server.crt", "go-grpc-tutorial")
	if err != nil {
		log.Fatalf("load crt fail: %v", err)
	}

	// 连接配置
	opts := []grpc.DialOption{
		grpc.WithTransportCredentials(cred),
	}
	conn, err := grpc.Dial("localhost:1234", opts...)
	if err != nil {
		log.Fatal(err)
	}

	...

Token 认证

TLS 认证是针对连接的安全加密方式,实际应用中还需要针对每个用户请求进行认证,常用的方式就是基于 token 认证,gRPC 使用 grpc.PerRPCCredentials 接口对此提供了支持。

// PerRPCCredentials defines the common interface for the credentials which need to
// attach security information to every RPC (e.g., oauth2).
type PerRPCCredentials interface {
	// GetRequestMetadata gets the current request metadata, refreshing
	// tokens if required. This should be called by the transport layer on
	// each request, and the data should be populated in headers or other
	// context. If a status code is returned, it will be used as the status
	// for the RPC. uri is the URI of the entry point for the request.
	// When supported by the underlying implementation, ctx can be used for
	// timeout and cancellation. Additionally, RequestInfo data will be
	// available via ctx to this call.
	// TODO(zhaoq): Define the set of the qualified keys instead of leaving
	// it as an arbitrary string.
	GetRequestMetadata(ctx context.Context, uri ...string) (map[string]string, error)
	// RequireTransportSecurity indicates whether the credentials requires
	// transport security.
	RequireTransportSecurity() bool
}

客户端示例如下:

// src/auth/client.go

func Ping() {
	// 增加认证 Dial Option
	conn, err := grpc.Dial("localhost:1234", grpc.WithPerRPCCredentials(CustomAuth{Token: "1234567890"}))
	if err != nil {
		log.Fatal(err)
	}
	defer conn.Close()

	// 实例化客户端并调用
	client := pb.NewPingPongClient(conn)
	res, err := client.Ping(context.Background(), &pb.PingRequest{Value: "ping"})
	if err != nil {
		log.Fatal(err)
	}
	log.Println(res.Value)
}

// CustomAuth 自定义认证类型
type CustomAuth struct {
	Token string
}

// GetRequestMetadata 生成认证信息
func (a CustomAuth) GetRequestMetadata(ctx context.Context, uri ...string) (map[string]string, error) {
	return map[string]string{
		"authorization": a.Token,
	}, nil
}

// RequireTransportSecurity 是否开启 TLS
func (a CustomAuth) RequireTransportSecurity() bool {
	return false
}

这里定义了一个 CustomAuth 类型,并实现了 grpc.PerRPCCredentials 接口的两个方法,通过 grpc.WithPerRPCCredentials 方法转换为 DialOption 类型初始化连接,这样每次 rpc 调用时 token 信息会通过请求的metadata 传输到服务端。

既然是通过 metadata 传输 token 信息,那么服务端认证就非常简单了,可以实现一个拦截器统一处理请求中的 token,示例如下:

// src/auth/server.go
...

// 服务端拦截器 - token 认证
func authInterceptor(ctx context.Context, req interface{}, info *grpc.UnaryServerInfo, handler grpc.UnaryHandler) (resp interface{}, err error) {
	md, ok := metadata.FromIncomingContext(ctx)
	if !ok {
		return nil, errors.New("authorization missing")
	}

	var token string
	if auth, ok := md["authorization"]; ok {
		token = auth[0]
	}
	if token != "1234567890" {
		return nil, grpc.Errorf(codes.Unauthenticated, "token invalid")
	}

	// 处理请求
	return handler(ctx, req)
}

// 启动server
func main() {
	srv := grpc.NewServer(grpc.UnaryInterceptor(authInterceptor))

...

以上就是基于 token 的认证方法,还是比较简单的,实际应用中可以根据自己的业务需求生成不同类型的 token,google.golang.org/grpc/credentials/oauth包也对 oauth2 和 jwt 提供了支持,感兴趣可以看一下 oauth.NewOauthAccess(token *oauth2.Token)oauth.NewJWTAccessFromKey(jsonKey []byte) 方法,实际上也是实现了 grpc.PerRPCCredentials 接口。


gRPC 生态


gRPC 的发展非常迅速,从 gRPC 扩展出来的生态包含了各种各样的工具,这部分介绍几个常用的工具。

项目地址:grpc-ecosystem

gRPC Gateway


项目地址:grpc-gateway

源自coreos的一篇博客 Take a REST with HTTP/2, Protobufs, and Swagger

etcd3 API全面升级为gRPC后,同时要提供REST API服务,维护两个版本的服务显然不太合理,所以grpc-gateway诞生了。通过protobuf的自定义option实现了一个网关,服务端同时开启gRPC和HTTP服务,HTTP服务接收客户端请求后转换为grpc请求数据,获取响应后转为json数据返回给客户端。

结构如图:

安装grpc-gateway

$ go get -u github.com/grpc-ecosystem/grpc-gateway/protoc-gen-grpc-gateway

目录结构

|—— hello_http/
	|—— client/
    	|—— main.go   // 客户端
	|—— server/
    	|—— main.go   // GRPC服务端
	|—— server_http/
		|—— main.go   // HTTP服务端
|—— proto/
	|—— google       // googleApi http-proto定义
		|—— api
			|—— annotations.proto
			|—— annotations.pb.go
			|—— http.proto
			|—— http.pb.go
	|—— hello_http/
		|—— hello_http.proto   // proto描述文件
		|—— hello_http.pb.go   // proto编译后文件
		|—— hello_http_pb.gw.go // gateway编译后文件

这里用到了google官方Api中的两个proto描述文件,直接拷贝不要做修改,里面定义了protocol buffer扩展的HTTP option,为grpc的http转换提供支持。

示例代码

Step 1. 编写proto描述文件:proto/hello_http.proto

syntax = "proto3";

package hello_http;
option go_package = "hello_http";

import "google/api/annotations.proto";

// 定义Hello服务
service HelloHTTP {
    // 定义SayHello方法
    rpc SayHello(HelloHTTPRequest) returns (HelloHTTPResponse) {
        // http option
        option (google.api.http) = {
            post: "/example/echo"
            body: "*"
        };
    }
}

// HelloRequest 请求结构
message HelloHTTPRequest {
    string name = 1;
}

// HelloResponse 响应结构
message HelloHTTPResponse {
    string message = 1;
}

这里在原来的SayHello方法定义中增加了http option, POST方式,路由为"/example/echo"。

Step 2. 编译proto

$ cd proto

# 编译google.api
$ protoc -I . --go_out=plugins=grpc,Mgoogle/protobuf/descriptor.proto=github.com/golang/protobuf/protoc-gen-go/descriptor:. google/api/*.proto

# 编译hello_http.proto
$ protoc -I . --go_out=plugins=grpc,Mgoogle/api/annotations.proto=github.com/jergoo/go-grpc-tutorial/proto/google/api:. hello_http/*.proto

# 编译hello_http.proto gateway
$ protoc --grpc-gateway_out=logtostderr=true:. hello_http/hello_http.proto

注意这里需要编译google/api中的两个proto文件,同时在编译hello_http.proto时使用M参数指定引入包名,最后使用grpc-gateway编译生成hello_http_pb.gw.go文件,这个文件就是用来做协议转换的,查看文件可以看到里面生成的http handler,处理proto文件中定义的路由"example/echo"接收POST参数,调用HelloHTTP服务的客户端请求grpc服务并响应结果。

Step 3: 实现服务端和客户端

server/main.go和client/main.go的实现与hello项目一致,这里不再说明。

server_http/main.go

package main

import (
	"net/http"

	"github.com/grpc-ecosystem/grpc-gateway/runtime"
	"golang.org/x/net/context"
	"google.golang.org/grpc"
	"google.golang.org/grpc/grpclog"

	gw "github.com/jergoo/go-grpc-tutorial/proto/hello_http"
)

func main() {
	ctx := context.Background()
	ctx, cancel := context.WithCancel(ctx)
	defer cancel()

	// grpc服务地址
	endpoint := "127.0.0.1:50052"
	mux := runtime.NewServeMux()
	opts := []grpc.DialOption{grpc.WithInsecure()}

	// HTTP转grpc
	err := gw.RegisterHelloHTTPHandlerFromEndpoint(ctx, mux, endpoint, opts)
	if err != nil {
		grpclog.Fatalf("Register handler err:%v\n", err)
	}

	grpclog.Println("HTTP Listen on 8080")
	http.ListenAndServe(":8080", mux)
}

就是这么简单。开启了一个http server,收到请求后根据路由转发请求到对应的RPC接口获得结果。grpc-gateway做的事情就是帮我们自动生成了转换过程的实现。

运行结果

依次开启gRPC服务和HTTP服务端:

$ cd hello_http/server && go run main.go
Listen on 127.0.0.1:50052
$ cd hello_http/server_http && go run main.go
HTTP Listen on 8080

调用grpc客户端:

$ cd hello_http/client && go run main.go
Hello gRPC.

# HTTP 请求
$ curl -X POST -k http://localhost:8080/example/echo -d '{"name": "gRPC-HTTP is working!"}'
{"message":"Hello gRPC-HTTP is working!."}

升级版服务端

上面的使用方式已经实现了我们最初的需求,grpc-gateway项目中提供的示例也是这种使用方式,这样后台需要开启两个服务两个端口。其实我们也可以只开启一个服务,同时提供http和gRPC调用方式。

新建一个项目hello_http_2, 基于hello_tls项目改造。客户端只要修改调用的proto包地址就可以了,这里我们看服务端的实现:

hello_http_2/server/main.go

package main

import (
	"crypto/tls"
	"io/ioutil"
	"net"
	"net/http"
	"strings"

	"github.com/grpc-ecosystem/grpc-gateway/runtime"
	pb "github.com/jergoo/go-grpc-tutorial/proto/hello_http"
	"golang.org/x/net/context"
	"golang.org/x/net/http2"
	"google.golang.org/grpc"
	"google.golang.org/grpc/credentials"
	"google.golang.org/grpc/grpclog"
)

// 定义helloHTTPService并实现约定的接口
type helloHTTPService struct{}

// HelloHTTPService Hello HTTP服务
var HelloHTTPService = helloHTTPService{}

// SayHello 实现Hello服务接口
func (h helloHTTPService) SayHello(ctx context.Context, in *pb.HelloHTTPRequest) (*pb.HelloHTTPResponse, error) {
	resp := new(pb.HelloHTTPResponse)
	resp.Message = "Hello " + in.Name + "."

	return resp, nil
}

func main() {
	endpoint := "127.0.0.1:50052"
	conn, err := net.Listen("tcp", endpoint)
	if err != nil {
		grpclog.Fatalf("TCP Listen err:%v\n", err)
	}

	// grpc tls server
	creds, err := credentials.NewServerTLSFromFile("../../keys/server.pem", "../../keys/server.key")
	if err != nil {
		grpclog.Fatalf("Failed to create server TLS credentials %v", err)
	}
	grpcServer := grpc.NewServer(grpc.Creds(creds))
	pb.RegisterHelloHTTPServer(grpcServer, HelloHTTPService)

	// gw server
	ctx := context.Background()
	dcreds, err := credentials.NewClientTLSFromFile("../../keys/server.pem", "server name")
	if err != nil {
		grpclog.Fatalf("Failed to create client TLS credentials %v", err)
	}
	dopts := []grpc.DialOption{grpc.WithTransportCredentials(dcreds)}
	gwmux := runtime.NewServeMux()
	if err = pb.RegisterHelloHTTPHandlerFromEndpoint(ctx, gwmux, endpoint, dopts); err != nil {
		grpclog.Fatalf("Failed to register gw server: %v\n", err)
	}

	// http服务
	mux := http.NewServeMux()
	mux.Handle("/", gwmux)

	srv := &http.Server{
		Addr:      endpoint,
		Handler:   grpcHandlerFunc(grpcServer, mux),
		TLSConfig: getTLSConfig(),
	}

	grpclog.Infof("gRPC and https listen on: %s\n", endpoint)

	if err = srv.Serve(tls.NewListener(conn, srv.TLSConfig)); err != nil {
		grpclog.Fatal("ListenAndServe: ", err)
	}

	return
}

func getTLSConfig() *tls.Config {
	cert, _ := ioutil.ReadFile("../../keys/server.pem")
	key, _ := ioutil.ReadFile("../../keys/server.key")
	var demoKeyPair *tls.Certificate
	pair, err := tls.X509KeyPair(cert, key)
	if err != nil {
		grpclog.Fatalf("TLS KeyPair err: %v\n", err)
	}
	demoKeyPair = &pair
	return &tls.Config{
		Certificates: []tls.Certificate{*demoKeyPair},
		NextProtos:   []string{http2.NextProtoTLS}, // HTTP2 TLS支持
	}
}

// grpcHandlerFunc returns an http.Handler that delegates to grpcServer on incoming gRPC
// connections or otherHandler otherwise. Copied from cockroachdb.
func grpcHandlerFunc(grpcServer *grpc.Server, otherHandler http.Handler) http.Handler {
	if otherHandler == nil {
		return http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
			grpcServer.ServeHTTP(w, r)
		})
	}
	return http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
		if r.ProtoMajor == 2 && strings.Contains(r.Header.Get("Content-Type"), "application/grpc") {
			grpcServer.ServeHTTP(w, r)
		} else {
			otherHandler.ServeHTTP(w, r)
		}
	})
}

gRPC服务端接口的实现没有区别,重点在于HTTP服务的实现。gRPC是基于http2实现的,net/http包也实现了http2,所以我们可以开启一个HTTP服务同时服务两个版本的协议,在注册http handler的时候,在方法grpcHandlerFunc中检测请求头信息,决定是直接调用gRPC服务,还是使用gateway的HTTP服务。net/http中对http2的支持要求开启https,所以这里要求使用https服务。

步骤

  • 注册开启TLS的grpc服务
  • 注册开启TLS的gateway服务,地址指向grpc服务
  • 开启HTTP server

运行结果

$ cd hello_http_2/server && go run main.go
gRPC and https listen on: 127.0.0.1:50052
$ cd hello_http_2/client && go run main.go
Hello gRPC.

# HTTP 请求
$ curl -X POST -k https://localhost:50052/example/echo -d '{"name": "gRPC-HTTP is working!"}'
{"message":"Hello gRPC-HTTP is working!."}

gRPC Middleware


项目地址:go-grpc-middleware

grpcurl


项目地址:grpcurl

参考


Doc & Book

Projects