Go中的gRPC简介

原文链接: https://mp.weixin.qq.com/s?__biz=MzAxMTA4Njc0OQ==&mid=2651437222&idx=1&sn=7acf20b316dce8c031d818d4ece808d5&chksm=80bb6654b7ccef4219594fe3b9cb61528938d8870fc069be3a8d219a63d8070402da3e6eb0f2&scene=0&xtrack=1&key=f84adc0dd5edf8f12304199fb388f5143df80b3f3177

给使用 Go 语言的初学者的 gRPC 概述

文章目录

  • RPC
    • 它是怎么工作的?
  • 用例
  • Protocol Buffers
    • 安装
  • gRPC
    • 实现
  • 总结
    • 架构
    • 向后兼容性
    • 架构演变
    • 验证
    • 语言互操作性

RPC

RPC 是用于 软件应用之间点对点通信网络编程模型 或是 进程间通信技术

RPC 是一种 协议,一个程序能够使用该协议,对位于另外一台计算机中的程序请求服务,而无需了解网络的详细信息。

RPC 代表 “远程过程调用”,它是一种 客户端 - 服务器交互 的形式 - 调用者是客户端,执行者是服务器 - 通常通过 " 请求 - 响应消息传递系统 " 实现。

客户端运行时程序,知道如何去寻址远程服务器应用程序,以及通过网络发送请求远程过程的消息。类似的,服务器包括与远程过程本身的运行时程序和存根。

它是怎么工作的?

RPC 工作的方式是,发送方或者客户端以过程、函数或者方法调用的形式创建对 RPC 进行转换和发送的远程服务器的请求。当远程服务器接收到请求时,它会将响应发送回客户端,然后应用程序继续其进程。

RPC 工作的方式是,发送方或者客户端以过程、函数或者方法调用的形式创建对 RPC 进行转换和发送的远程服务器的请求。当远程服务器接收到请求时,它会将响应发送回客户端,然后应用程序继续其进程。

Go中的gRPC简介_第1张图片

用例

我们将实现一个 Gravatar 服务,用以 生成 URLs,其包含相关邮件地址的 MD5 哈希。它们可用于从 Gravatar Web 服务器加载全局唯一的头像。

我们的客户端能够通过 RPC 协议与服务器通信,发送电子邮件和所需要的图像。作为响应,他们将获得一个在 https://gravatar.com 上配置的他们自己头像的个性化链接。

Go中的gRPC简介_第2张图片

Protocol Buffers

Protobuf(或 Protocol Buffers)是 Google 发明的 与语言无关且与平台无关的序列化形式,每个 Protocol Buffers 消息都是一个 小的逻辑信息记录包含一系列的 name-value 对

与 XML 或者 JSON 不同,在这里你首先在一个 .proto 文件中定义模式。它们是一种类似 JSON 但更简单,更小,严格类型的格式,只有从客户端到服务器才能理解,而且 Marshall/Unmarshall 更快。例如:

 1	syntax = "proto3";
 2
 3	package gravatar;
 4
 5	service GravatarService {
 6    rpc Generate(GravatarRequest) returns (GravatarResponse) {}
 7	}
 8
 9	message GravatarRequest {
10    string email = 1;
11    int32 size = 2;
12	}
13
14	message GravatarResponse {
15    string url = 1;
16	}

一个消息类型是一个数值字段的列表,每一个字段有一个类型和一个名称。在定义了 .proto 文件之后,运行 protocol buffer 编译器去给对象(使用你选择的语言)生成代码,使用字段的 get/set 函数以及对象的序列化 / 反序列化函数。如你所见,你也可以在命名空间内打包信息。

安装

我们使用 protoc 编译器编译一个 protocol buffer,目标文件就是为一门编程语言生成的。对于 Go,编译器会为你的文件中的每一个消息类型生成一个 .pb.go 文件。

要安装编译器,运行:

1	brew install protobuf

然后,在你的 GOPATH 路径下创建并初始化一个新项目:

1	mkdir profobuf-example
2	cd profobuf-example
3	go mod INIt

最后,编译所有的 .proto 文件:

1	protoc --go_out=. *.proto

我编译后的文件如下所示:

  1	// Code generated by protoc-gen-go. DO NOT EDIT.
  2	// source: gravatar.proto
  3
  4	package gravatar
  5
  6	import proto "github.com/golang/protobuf/proto"
  7	import fmt "fmt"
  8	import math "math"
  9
 10	// Reference imports to suppress errors if they are not otherwise used.
 11	var _ = proto.Marshal
 12	var _ = fmt.Errorf
 13	var _ = math.Inf
 14
 15	// This is a compile-time assertion to ensure that this generated file
 16	// is compatible with the proto package it is being compiled against.
 17	// A compilation error at this line likely means your copy of the
 18	// proto package needs to be updated.
 19	const _ = proto.ProtoPackageIsVersion2 // please upgrade the proto package
 20
 21	type GravatarRequest struct {
 22    Email                string   `protobuf:"bytes,1,opt,name=email,proto3" JSON:"email,omitempty"`
 23    Size                 int32    `protobuf:"varint,2,opt,name=size,proto3" JSON:"size,omitempty"`
 24    XXX_NoUnkeyedLiteral struct{} `json:"-"`
 25    XXX_unrecognized     []byte   `json:"-"`
 26    XXX_sizecache        int32    `json:"-"`
 27	}
 28
 29	func (m *GravatarRequest) Reset()         { *m = GravatarRequest{} }
 30	func (m *GravatarRequest) String() string { return proto.CompactTextString(m) }
 31	func (*GravatarRequest) ProtoMessage()    {}
 32	func (*GravatarRequest) Descriptor() ([]byte, []int) {
 33    return fileDescriptor_gravatar_d539f97f43eb2d2e, []int{0}
 34	}
 35	func (m *GravatarRequest) XXX_Unmarshal(b []byte) error {
 36    return xxx_messageInfo_GravatarRequest.Unmarshal(m, b)
 37	}
 38	func (m *GravatarRequest) XXX_Marshal(b []byte, deterministic bool) ([]byte, error) {
 39    return xxx_messageInfo_GravatarRequest.Marshal(b, m, deterministic)
 40	}
 41	func (dst *GravatarRequest) XXX_Merge(src proto.Message) {
 42    xxx_messageInfo_GravatarRequest.Merge(dst, src)
 43	}
 44	func (m *GravatarRequest) XXX_Size() int {
 45    return xxx_messageInfo_GravatarRequest.Size(m)
 46	}
 47	func (m *GravatarRequest) XXX_DiscardUnknown() {
 48    xxx_messageInfo_GravatarRequest.DiscardUnknown(m)
 49	}
 50
 51	var xxx_messageInfo_GravatarRequest proto.InternalMessageInfo
 52
 53	func (m *GravatarRequest) GetEmail() string {
 54    if m != nil {
 55        return m.Email
 56    }
 57    return ""
 58	}
 59
 60	func (m *GravatarRequest) GetSize() int32 {
 61    if m != nil {
 62        return m.Size
 63    }
 64    return 0
 65	}
 66
 67	type GravatarResponse struct {
 68    Url                  string   `protobuf:"bytes,1,opt,name=url,proto3" JSON:"url,omitempty"`
 69    XXX_NoUnkeyedLiteral struct{} `json:"-"`
 70    XXX_unrecognized     []byte   `json:"-"`
 71    XXX_sizecache        int32    `json:"-"`
 72	}
 73
 74	func (m *GravatarResponse) Reset()         { *m = GravatarResponse{} }
 75	func (m *GravatarResponse) String() string { return proto.CompactTextString(m) }
 76	func (*GravatarResponse) ProtoMessage()    {}
 77	func (*GravatarResponse) Descriptor() ([]byte, []int) {
 78    return fileDescriptor_gravatar_d539f97f43eb2d2e, []int{1}
 79	}
 80	func (m *GravatarResponse) XXX_Unmarshal(b []byte) error {
 81    return xxx_messageInfo_GravatarResponse.Unmarshal(m, b)
 82	}
 83	func (m *GravatarResponse) XXX_Marshal(b []byte, deterministic bool) ([]byte, error) {
 84    return xxx_messageInfo_GravatarResponse.Marshal(b, m, deterministic)
 85	}
 86	func (dst *GravatarResponse) XXX_Merge(src proto.Message) {
 87    xxx_messageInfo_GravatarResponse.Merge(dst, src)
 88	}
 89	func (m *GravatarResponse) XXX_Size() int {
 90    return xxx_messageInfo_GravatarResponse.Size(m)
 91	}
 92	func (m *GravatarResponse) XXX_DiscardUnknown() {
 93    xxx_messageInfo_GravatarResponse.DiscardUnknown(m)
 94	}
 95
 96	var xxx_messageInfo_GravatarResponse proto.InternalMessageInfo
 97
 98	func (m *GravatarResponse) GetUrl() string {
 99    if m != nil {
100        return m.Url
101    }
102    return ""
103	}
104
105	func INIt() {
106    proto.RegisterType((*GravatarRequest)(nil), "gravatar.GravatarRequest")
107    proto.RegisterType((*GravatarResponse)(nil), "gravatar.GravatarResponse")
108	}
109
110	func INIt() { proto.RegisterFile("gravatar.proto", fileDescriptor_gravatar_d539f97f43eb2d2e) }
111
112	var fileDescriptor_gravatar_d539f97f43eb2d2e = []byte{
113    // 158 bytes of a gzipped FileDescriptorProto
114    0x1f, 0x8b, 0x08, 0x00, 0x00, 0x00, 0x00, 0x00, 0x02, 0xff, 0xe2, 0xe2, 0x4b, 0x2f, 0x4a, 0x2c,
115    0x4b, 0x2c, 0x49, 0x2c, 0xd2, 0x2b, 0x28, 0xca, 0x2f, 0xc9, 0x17, 0xe2, 0x80, 0xf1, 0x95, 0xac,
116    0xb9, 0xf8, 0xdd, 0xa1, 0xec, 0xa0, 0xd4, 0xc2, 0xd2, 0xd4, 0xe2, 0x12, 0x21, 0x11, 0x2e, 0xd6,
117    0xd4, 0xdc, 0xc4, 0xcc, 0x1c, 0x09, 0x46, 0x05, 0x46, 0x0d, 0xce, 0x20, 0x08, 0x47, 0x48, 0x88,
118    0x8b, 0xa5, 0x38, 0xb3, 0x2a, 0x55, 0x82, 0x49, 0x81, 0x51, 0x83, 0x35, 0x08, 0xcc, 0x56, 0x52,
119    0xe1, 0x12, 0x40, 0x68, 0x2e, 0x2e, 0xc8, 0xcf, 0x2b, 0x4e, 0x15, 0x12, 0xe0, 0x62, 0x2e, 0x2d,
120    0x82, 0xe9, 0x05, 0x31, 0x8d, 0xc2, 0x10, 0x56, 0x04, 0xa7, 0x16, 0x95, 0x65, 0x26, 0xa7, 0x0a,
121    0x39, 0x73, 0x71, 0xb8, 0xa7, 0xe6, 0xa5, 0x16, 0x25, 0x96, 0xa4, 0x0a, 0x49, 0xea, 0xc1, 0x1d,
122    0x87, 0xe6, 0x12, 0x29, 0x29, 0x6c, 0x52, 0x10, 0x7b, 0x94, 0x18, 0x92, 0xd8, 0xc0, 0x7e, 0x31,
123    0x06, 0x04, 0x00, 0x00, 0xff, 0xff, 0xfd, 0xd3, 0xc0, 0x5b, 0xdd, 0x00, 0x00, 0x00,
124}

gRPC

gRPC 是一个高性能的 RPC 框架,它使用 protocol buffers(作为其接口定义语言和基础消息交换格式)和 HTTP/2 构建。

一旦你指定了你的数据结构,你就可以在普通 .proto 文件中定义 gRPC 服务,并将 RPC 方法参数和返回类型指定为 protocol buffers 消息。在我们的例子中,它就是:

1	service GravatarService {
2    rpc Generate(GravatarRequest) returns (GravatarResponse) {}
3	}

当你使用 protoc(一个 gRPC 插件)从你的 proto 文件生成代码时,你不仅可以获得用于填充、序列化和检索消息类型的常规 protocol buffers 代码,还可以生成 gRPC 客户端和服务器代码。要做到这一点,只需运行:

1	protoc --go_out=plugins=grpc:. *.proto

差异如下:

 1+ import (
 2+     context "golang.org/x/net/context"
 3+     grpc "google.golang.org/grpc"
 4+ )
 5+
 6+ // Reference imports to suppress errors if they are not otherwise used.
 7+ var _ context.Context
 8+ var _ grpc.ClientConn
 9+
10+ // This is a compile-time assertion to ensure that this generated file
11+ // is compatible with the grpc package it is being compiled against.
12+ const _ = grpc.SupportPackageIsVersion4
13+
14+ // GravatarServiceClient is the client API for GravatarService service.
15+ //
16+ // For semantics around ctx use and closing/ending streaming RPCs, please refer to https://godoc.org/google.golang.org/grpc#ClientConn.NewStream.
17+ type GravatarServiceClient interface {
18+     Generate(ctx context.Context, in *GravatarRequest, opts ...grpc.CallOption) (*GravatarResponse, error)
19+ }
20+
21+ type gravatarServiceClient struct {
22+     cc *grpc.ClientConn
23+ }
24+
25+ func NewGravatarServiceClient(cc *grpc.ClientConn) GravatarServiceClient {
26+     return &gravatarServiceClient{cc}
27+ }
28+
29+ func (c *gravatarServiceClient) Generate(ctx context.Context, in *GravatarRequest, opts ...grpc.CallOption) (*GravatarResponse, error) {
30+     out := new(GravatarResponse)
31+     err := c.cc.Invoke(ctx, "/gravatar.GravatarService/Generate", in, out, opts...)
32+     if err != nil {
33+         return nil, err
34+     }
35+     return out, nil
36+ }
37+
38+ // GravatarServiceServer is the server API for GravatarService service.
39+ type GravatarServiceServer interface {
40+     Generate(context.Context, *GravatarRequest) (*GravatarResponse, error)
41+ }
42+
43+ func RegisterGravatarServiceServer(s *grpc.Server, srv GravatarServiceServer) {
44+     s.RegisterService(&_GravatarService_serviceDesc, srv)
45+ }
46+
47+ func _GravatarService_Generate_Handler(srv interface{}, ctx context.Context, dec func(interface{}) error, interceptor grpc.UnaryServerInterceptor) (interface{}, error) {
48+     in := new(GravatarRequest)
49+     if err := dec(in); err != nil {
50+         return nil, err
51+     }
52+     if interceptor == nil {
53+         return srv.(GravatarServiceServer).Generate(ctx, in)
54+     }
55+     info := &grpc.UnaryServerInfo{
56+         Server:     srv,
57+         FullMethod: "/gravatar.GravatarService/Generate",
58+     }
59+     handler := func(ctx context.Context, req interface{}) (interface{}, error) {
60+         return srv.(GravatarServiceServer).Generate(ctx, req.(*GravatarRequest))
61+     }
62+     return interceptor(ctx, in, info, handler)
63+ }
64+
65+ var _GravatarService_serviceDesc = grpc.ServiceDesc{
66+     ServiceName: "gravatar.GravatarService",
67+     HandlerType: (*GravatarServiceServer)(nil),
68+     Methods: []grpc.MethodDesc{
69+         {
70+             MethodName: "Generate",
71+             Handler:    _GravatarService_Generate_Handler,
72+         },
73+     },
74+     Streams:  []grpc.StreamDesc{},
75+     Metadata: "gravatar.proto",
76+ }

实现

现在我们已经生成了服务器和客户端代码,因此我们需要在应用中去实现并且调用这些方法。

让我们开始为我们的“核心业务”实现基础的逻辑:

 1	func gravatarHash(email string) [16]byte {
 2    return md5.Sum([]byte(email))
 3	}
 4
 5	func gravatarURL(hash [16]byte, size uint32) string {
 6    return fmt.Sprintf("https://www.gravatar.com/avatar/%x?s=%d", hash, size)
 7	}
 8
 9	func gravatar(email string, size uint32) string {
10    hash := gravatarHash(email)
11
12    return gravatarURL(hash, size)
13	}
 1	import (
 2    "fmt"
 3    "github.com/stretchr/testify/assert"
 4    "testing"
 5)
 6
 7	func TestGravatar(t *testing.T) {
 8    var size uint32 = 10
 9    endpoint := "https://www.gravatar.com/avatar/cf38500a2cd3b6a2c8c1d4d8259e83f8?s=%v"
10    email := "[email protected]"
11    url := gravatar(email, size)
12    expected := fmt.Sprintf(endpoint, size)
13
14    assert.Equal(t, url, expected, "URLs are not the same.")
15	}

它并没有什么特别之处,只是 GO 中常规的 MD5 生成。

但是,服务器端的实现更加有趣:

 1	const port = ":50051"
 2
 3	type gravatarService struct{}
 4
 5	func (s *gravatarService) Generate(ctx context.Context, in *pb.GravatarRequest) (*pb.GravatarResponse, error) {
 6    log.Printf("Received email %v with size %v", in.Email, in.Size)
 7    return &pb.GravatarResponse{Url: gravatar(in.Email, in.Size)}, nil
 8	}
 9
10	func main() {
11    lis, err := net.Listen("tcp", port)
12    if err != nil {
13        log.Fatal(errors.Wrap(err, "Failed to listen on port!"))
14    }
15
16    server := grpc.NewServer()
17    pb.RegisterGravatarServiceServer(server, &gravatarService{})
18    if err := server.Serve(lis); err != nil {
19        log.Fatal(errors.Wrap(err, "Failed to start server!"))
20    }
21	}

我们定义了运行服务器的端口,同时 gravatarService 的结构覆盖了通过 .proto 文件定义的 GravatarService。如你所见,我们还可以在其上实现需要的 Generate 方法,它接收 GravatarRequest 并且产生一个相应的 GravatarResponse。

我们在给定端口打开一个 tcp 连接,创建一个新的 gRPC 服务器,它注册我们的处理程序并在打开的监听器上启动它。我们现在准备去处理请求。

客户端的实现也不难,我会说更容易一些:

 1	const address = "localhost:50051"
 2
 3	func main() {
 4    conn, err := grpc.Dial(address, grpc.WithInsecure())
 5    if err != nil {
 6        log.Fatalf("did not connect: %v", err)
 7    }
 8    defer conn.Close()
 9
10    c := pb.NewGravatarServiceClient(conn)
11    ctx, cancel := context.WithTimeout(context.Background(), time.Second)
12    defer cancel()
13
14    r, err := c.Generate(ctx, &pb.GravatarRequest{Email: "name", Size: 10})
15    if err != nil {
16        log.Fatalf("could not greet: %v", err)
17    }
18
19    log.Printf("Greeting: %s", r.Url)
20	}

我们在给定的地址上开启一个特定的连接(在我们的例子中,它就是 localhost 与先前定义的端口),我们在给定的连接上注册一个新的客户端。记住,当退出我们的程序时,必须要同时关闭连接并停掉 context。

最后,在我们的客户端使用 GravatarRequest 调用 Generate 方法,同时我们的数据也在里面。如果成功,我们可以使用哈希打印接收到的 URL。

总结

Protocol Buffers 在编码和解码速度,线路上数据大小等方面提供了非常实际的优势。现在你可能想知道,gRPC 相对于常规的 JSON REST API 有什么优势呢。让我们考虑几件事:

架构

我们通常依赖在系统之间的边界上不一致的代码。它没有强制我们的组件结构,这很重要。一旦以 proto 格式编码了业务对象的语义,就足够确保信号不会在应用程序之间丢失,并且你创建的边界符合你的业务规则。

向后兼容性

使用数值字段,你永远不必要更改代码的行为去保持与旧版本的向后兼容性。正如文档所述,一旦引入 Protocol Buffers:

“可以轻松引入新的字段,而中间服务器不需要检查数据,也能简单地解析它并且传递数据而无需了解所有字段。”

架构演变

Protocol Buffers 生成的存根类(你通常不必要接触)可以提供大部分 JSON 功能,而不会让你头疼。随着你的架构与你的 proto 生成的类一起发展(一旦你重新生成它们,不可否认),为你留出更多空间来专注于保持应用程序的运行和构建产品的挑战。

验证

在 Protocol Buffers 中定义的 required,optional 和 repeated 关键字是非常强大的。它们允许你在架构级别对你的数据结构形状进行编码,并为你处理每种语言中类的工作方式的实现细节。例如,如果你尝试对没有填写必填字段的对象实例进行编码,则库将引发异常。你还可以通过简单地滚动到新的数值字段来将字段从 required 更改为 optional 或者反过来。拥有这种对序列化格式语义的灵活编码是非常强大的。

语言互操作性

由于 Protocol Buffers 以各种语言实现,因此它们使架构中的多语言应用程序之间的互操作性变得更加简单。如果你在 NodeJS,Go 或甚至 Elixir 中引入新服务,你只需要将 proto 文件交给使用目标语言编写的代码生成器,你就为这些体系结构之间的安全性和互操作性提供了一些很好的保证。

你可以说你仍然会在一些简单的情况下使用 JSON,我同意,并没有完全的替换 JSON,特别是对于直接由 Web 浏览器使用的服务。我希望你能在自己的用例中为它们找到合适的位置。

Go中的gRPC简介_第3张图片

你可能感兴趣的:(go语言)