Go 中的 gRPC 简介

PotoYang · · 1410 次点击 · · 开始浏览    
这是一个创建于 的文章,其中的信息可能已经有所发展或是发生改变。

*给使用 Go 语言的初学者的 gRPC 概述* ![grpc.png](https://raw.githubusercontent.com/studygolang/gctt-images/master/a-brief-introduction-to-grpc-in-go/grpc.png) ## RPC RPC 是用于 **软件应用之间点对点通信** 的 **网络编程模型** 或是 **进程间通信技术**。 RPC 是一种 **协议**,一个程序能够使用该协议,对位于另外一台计算机中的程序请求服务,而无需了解网络的详细信息。 RPC 代表 **“远程过程调用”**,它是一种 **客户端 - 服务器交互** 的形式 - 调用者是客户端,执行者是服务器 - 通常通过 **" 请求 - 响应消息传递系统 "** 实现。 客户端运行时程序,知道如何去寻址远程服务器应用程序,以及通过网络发送请求远程过程的消息。类似的,服务器包括与远程过程本身的运行时程序和存根。 ### 它是怎么工作的? RPC 工作的方式是,发送方或者客户端以过程、函数或者方法调用的形式创建对 RPC 进行转换和发送的远程服务器的请求。当远程服务器接收到请求时,它会将响应发送回客户端,然后应用程序继续其进程。 当服务器处理调用或者请求时,客户端在恢复其进程之前等待服务器完成处理。但是,使用共享地址空间的轻量级进程或线程允许多个 RPC 并发地执行。 ![1_WXznZkgtv5gyO2vd4INUeA](https://raw.githubusercontent.com/studygolang/gctt-images/master/a-brief-introduction-to-grpc-in-go/1_WXznZkgtv5gyO2vd4INUeA.png) ## 用例 我们将实现一个 Gravatar 服务,用以 **生成 URLs**,其包含相关邮件地址的 MD5 哈希。它们可用于从 Gravatar Web 服务器加载全局唯一的头像。 我们的客户端能够通过 RPC 协议与服务器通信,发送电子邮件和所需要的图像。作为响应,他们将获得一个在 [https://gravatar.com](https://gravatar.com) 上配置的他们自己头像的个性化链接。 ![0_F0svelAqEuMPAIhF](https://raw.githubusercontent.com/studygolang/gctt-images/master/a-brief-introduction-to-grpc-in-go/0_F0svelAqEuMPAIhF.jpg) ## Protocol Buffers Protobuf(或 [Protocol Buffers](https://developers.google.com/protocol-buffers/docs/proto3))是 Google 发明的 **与语言无关且与平台无关的序列化形式**,每个 Protocol Buffers 消息都是一个 **小的逻辑信息记录,包含一系列的 name-value 对**。 与 `XML` 或者 `JSON` 不同,在这里你首先在一个 `.proto` 文件中定义模式。它们是一种类似 `JSON` 但更简单,更小,严格类型的格式,只有从客户端到服务器才能理解,而且 Marshall/Unmarshall 更快。例如: ```protobuf syntax = "proto3"; package gravatar; service GravatarService { rpc Generate(GravatarRequest) returns (GravatarResponse) {} } message GravatarRequest { string email = 1; int32 size = 2; } message GravatarResponse { string url = 1; } ``` **一个消息类型是一个数值字段的列表**,每一个字段有一个类型和一个名称。在定义了 `.proto` 文件之后,运行 protocol buffer 编译器去给对象(使用你选择的语言)生成代码,使用字段的 get/set 函数以及对象的序列化 / 反序列化函数。如你所见,你也可以在命名空间内打包信息。 ### 安装 我们使用 `protoc` 编译器编译一个 protocol buffer,目标文件就是为一门编程语言生成的。对于 Go,编译器会为你的文件中的每一个消息类型生成一个 `.pb.go` 文件。 要安装编译器,运行: ```shell brew install protobuf ``` 然后,在你的 `GOPATH` 路径下创建并初始化一个新项目: ```bash mkdir profobuf-example cd profobuf-example go mod INIt ``` 接下来,安装 Go 支持的 Google 的 protocol buffers: ```shell go get -u Github.com/golang/protobuf/protoc-gen-go go install Github.com/golang/protobuf/protoc-gen-go ``` 最后,编译所有的 `.proto` 文件: ```shell protoc --go_out=. *.proto ``` 我编译后的文件如下所示: ```go // Code generated by protoc-gen-go. DO NOT EDIT. // source: gravatar.proto package gravatar import proto "github.com/golang/protobuf/proto" import fmt "fmt" import math "math" // Reference imports to suppress errors if they are not otherwise used. var _ = proto.Marshal var _ = fmt.Errorf var _ = math.Inf // This is a compile-time assertion to ensure that this generated file // is compatible with the proto package it is being compiled against. // A compilation error at this line likely means your copy of the // proto package needs to be updated. const _ = proto.ProtoPackageIsVersion2 // please upgrade the proto package type GravatarRequest struct { Email string `protobuf:"bytes,1,opt,name=email,proto3" JSON:"email,omitempty"` Size int32 `protobuf:"varint,2,opt,name=size,proto3" JSON:"size,omitempty"` XXX_NoUnkeyedLiteral struct{} `json:"-"` XXX_unrecognized []byte `json:"-"` XXX_sizecache int32 `json:"-"` } func (m *GravatarRequest) Reset() { *m = GravatarRequest{} } func (m *GravatarRequest) String() string { return proto.CompactTextString(m) } func (*GravatarRequest) ProtoMessage() {} func (*GravatarRequest) Descriptor() ([]byte, []int) { return fileDescriptor_gravatar_d539f97f43eb2d2e, []int{0} } func (m *GravatarRequest) XXX_Unmarshal(b []byte) error { return xxx_messageInfo_GravatarRequest.Unmarshal(m, b) } func (m *GravatarRequest) XXX_Marshal(b []byte, deterministic bool) ([]byte, error) { return xxx_messageInfo_GravatarRequest.Marshal(b, m, deterministic) } func (dst *GravatarRequest) XXX_Merge(src proto.Message) { xxx_messageInfo_GravatarRequest.Merge(dst, src) } func (m *GravatarRequest) XXX_Size() int { return xxx_messageInfo_GravatarRequest.Size(m) } func (m *GravatarRequest) XXX_DiscardUnknown() { xxx_messageInfo_GravatarRequest.DiscardUnknown(m) } var xxx_messageInfo_GravatarRequest proto.InternalMessageInfo func (m *GravatarRequest) GetEmail() string { if m != nil { return m.Email } return "" } func (m *GravatarRequest) GetSize() int32 { if m != nil { return m.Size } return 0 } type GravatarResponse struct { Url string `protobuf:"bytes,1,opt,name=url,proto3" JSON:"url,omitempty"` XXX_NoUnkeyedLiteral struct{} `json:"-"` XXX_unrecognized []byte `json:"-"` XXX_sizecache int32 `json:"-"` } func (m *GravatarResponse) Reset() { *m = GravatarResponse{} } func (m *GravatarResponse) String() string { return proto.CompactTextString(m) } func (*GravatarResponse) ProtoMessage() {} func (*GravatarResponse) Descriptor() ([]byte, []int) { return fileDescriptor_gravatar_d539f97f43eb2d2e, []int{1} } func (m *GravatarResponse) XXX_Unmarshal(b []byte) error { return xxx_messageInfo_GravatarResponse.Unmarshal(m, b) } func (m *GravatarResponse) XXX_Marshal(b []byte, deterministic bool) ([]byte, error) { return xxx_messageInfo_GravatarResponse.Marshal(b, m, deterministic) } func (dst *GravatarResponse) XXX_Merge(src proto.Message) { xxx_messageInfo_GravatarResponse.Merge(dst, src) } func (m *GravatarResponse) XXX_Size() int { return xxx_messageInfo_GravatarResponse.Size(m) } func (m *GravatarResponse) XXX_DiscardUnknown() { xxx_messageInfo_GravatarResponse.DiscardUnknown(m) } var xxx_messageInfo_GravatarResponse proto.InternalMessageInfo func (m *GravatarResponse) GetUrl() string { if m != nil { return m.Url } return "" } func INIt() { proto.RegisterType((*GravatarRequest)(nil), "gravatar.GravatarRequest") proto.RegisterType((*GravatarResponse)(nil), "gravatar.GravatarResponse") } func INIt() { proto.RegisterFile("gravatar.proto", fileDescriptor_gravatar_d539f97f43eb2d2e) } var fileDescriptor_gravatar_d539f97f43eb2d2e = []byte{ // 158 bytes of a gzipped FileDescriptorProto 0x1f, 0x8b, 0x08, 0x00, 0x00, 0x00, 0x00, 0x00, 0x02, 0xff, 0xe2, 0xe2, 0x4b, 0x2f, 0x4a, 0x2c, 0x4b, 0x2c, 0x49, 0x2c, 0xd2, 0x2b, 0x28, 0xca, 0x2f, 0xc9, 0x17, 0xe2, 0x80, 0xf1, 0x95, 0xac, 0xb9, 0xf8, 0xdd, 0xa1, 0xec, 0xa0, 0xd4, 0xc2, 0xd2, 0xd4, 0xe2, 0x12, 0x21, 0x11, 0x2e, 0xd6, 0xd4, 0xdc, 0xc4, 0xcc, 0x1c, 0x09, 0x46, 0x05, 0x46, 0x0d, 0xce, 0x20, 0x08, 0x47, 0x48, 0x88, 0x8b, 0xa5, 0x38, 0xb3, 0x2a, 0x55, 0x82, 0x49, 0x81, 0x51, 0x83, 0x35, 0x08, 0xcc, 0x56, 0x52, 0xe1, 0x12, 0x40, 0x68, 0x2e, 0x2e, 0xc8, 0xcf, 0x2b, 0x4e, 0x15, 0x12, 0xe0, 0x62, 0x2e, 0x2d, 0x82, 0xe9, 0x05, 0x31, 0x8d, 0xc2, 0x10, 0x56, 0x04, 0xa7, 0x16, 0x95, 0x65, 0x26, 0xa7, 0x0a, 0x39, 0x73, 0x71, 0xb8, 0xa7, 0xe6, 0xa5, 0x16, 0x25, 0x96, 0xa4, 0x0a, 0x49, 0xea, 0xc1, 0x1d, 0x87, 0xe6, 0x12, 0x29, 0x29, 0x6c, 0x52, 0x10, 0x7b, 0x94, 0x18, 0x92, 0xd8, 0xc0, 0x7e, 0x31, 0x06, 0x04, 0x00, 0x00, 0xff, 0xff, 0xfd, 0xd3, 0xc0, 0x5b, 0xdd, 0x00, 0x00, 0x00, } ``` ## gRPC gRPC 是一个高性能的 RPC 框架,它使用 protocol buffers(作为其接口定义语言和基础消息交换格式)和 HTTP/2 构建。 一旦你指定了你的数据结构,你就可以在普通 `.proto` 文件中定义 gRPC 服务,并将 RPC 方法参数和返回类型指定为 protocol buffers 消息。在我们的例子中,它就是: ```protobuf service GravatarService { rpc Generate(GravatarRequest) returns (GravatarResponse) {} } ``` 当你使用 `protoc`(一个 gRPC 插件)从你的 proto 文件生成代码时,你不仅可以获得用于填充、序列化和检索消息类型的常规 protocol buffers 代码,还可以生成 gRPC 客户端和服务器代码。要做到这一点,只需运行: ```shell protoc --go_out=plugins=grpc:. *.proto ``` 差异如下: ```protobuf + import ( + context "golang.org/x/net/context" + grpc "google.golang.org/grpc" + ) + + // Reference imports to suppress errors if they are not otherwise used. + var _ context.Context + var _ grpc.ClientConn + + // This is a compile-time assertion to ensure that this generated file + // is compatible with the grpc package it is being compiled against. + const _ = grpc.SupportPackageIsVersion4 + + // GravatarServiceClient is the client API for GravatarService service. + // + // For semantics around ctx use and closing/ending streaming RPCs, please refer to https://godoc.org/google.golang.org/grpc#ClientConn.NewStream. + type GravatarServiceClient interface { + Generate(ctx context.Context, in *GravatarRequest, opts ...grpc.CallOption) (*GravatarResponse, error) + } + + type gravatarServiceClient struct { + cc *grpc.ClientConn + } + + func NewGravatarServiceClient(cc *grpc.ClientConn) GravatarServiceClient { + return &gravatarServiceClient{cc} + } + + func (c *gravatarServiceClient) Generate(ctx context.Context, in *GravatarRequest, opts ...grpc.CallOption) (*GravatarResponse, error) { + out := new(GravatarResponse) + err := c.cc.Invoke(ctx, "/gravatar.GravatarService/Generate", in, out, opts...) + if err != nil { + return nil, err + } + return out, nil + } + + // GravatarServiceServer is the server API for GravatarService service. + type GravatarServiceServer interface { + Generate(context.Context, *GravatarRequest) (*GravatarResponse, error) + } + + func RegisterGravatarServiceServer(s *grpc.Server, srv GravatarServiceServer) { + s.RegisterService(&_GravatarService_serviceDesc, srv) + } + + func _GravatarService_Generate_Handler(srv interface{}, ctx context.Context, dec func(interface{}) error, interceptor grpc.UnaryServerInterceptor) (interface{}, error) { + in := new(GravatarRequest) + if err := dec(in); err != nil { + return nil, err + } + if interceptor == nil { + return srv.(GravatarServiceServer).Generate(ctx, in) + } + info := &grpc.UnaryServerInfo{ + Server: srv, + FullMethod: "/gravatar.GravatarService/Generate", + } + handler := func(ctx context.Context, req interface{}) (interface{}, error) { + return srv.(GravatarServiceServer).Generate(ctx, req.(*GravatarRequest)) + } + return interceptor(ctx, in, info, handler) + } + + var _GravatarService_serviceDesc = grpc.ServiceDesc{ + ServiceName: "gravatar.GravatarService", + HandlerType: (*GravatarServiceServer)(nil), + Methods: []grpc.MethodDesc{ + { + MethodName: "Generate", + Handler: _GravatarService_Generate_Handler, + }, + }, + Streams: []grpc.StreamDesc{}, + Metadata: "gravatar.proto", + } ``` ### 实现 现在我们已经生成了服务器和客户端代码,因此我们需要在应用中去实现并且调用这些方法。 让我们开始为我们的“核心业务”实现基础的逻辑: ```go func gravatarHash(email string) [16]byte { return md5.Sum([]byte(email)) } func gravatarURL(hash [16]byte, size uint32) string { return fmt.Sprintf("https://www.gravatar.com/avatar/%x?s=%d", hash, size) } func gravatar(email string, size uint32) string { hash := gravatarHash(email) return gravatarURL(hash, size) } ``` ```go import ( "fmt" "github.com/stretchr/testify/assert" "testing" ) func TestGravatar(t *testing.T) { var size uint32 = 10 endpoint := "https://www.gravatar.com/avatar/cf38500a2cd3b6a2c8c1d4d8259e83f8?s=%v" email := "kamil@lelonek.me" url := gravatar(email, size) expected := fmt.Sprintf(endpoint, size) assert.Equal(t, url, expected, "URLs are not the same.") } ``` 它并没有什么特别之处,只是 GO 中常规的 MD5 生成。 但是,服务器端的实现更加有趣: ```go const port = ":50051" type gravatarService struct{} func (s *gravatarService) Generate(ctx context.Context, in *pb.GravatarRequest) (*pb.GravatarResponse, error) { log.Printf("Received email %v with size %v", in.Email, in.Size) return &pb.GravatarResponse{Url: gravatar(in.Email, in.Size)}, nil } func main() { lis, err := net.Listen("tcp", port) if err != nil { log.Fatal(errors.Wrap(err, "Failed to listen on port!")) } server := grpc.NewServer() pb.RegisterGravatarServiceServer(server, &gravatarService{}) if err := server.Serve(lis); err != nil { log.Fatal(errors.Wrap(err, "Failed to start server!")) } } ``` 我们定义了运行服务器的端口,同时 `gravatarService` 的结构覆盖了通过 `.proto` 文件定义的 `GravatarService`。如你所见,我们还可以在其上实现需要的 `Generate` 方法,它接收 `GravatarRequest` 并且产生一个相应的 `GravatarResponse`。 我们在给定端口打开一个 `tcp` 连接,创建一个新的 gRPC 服务器,它注册我们的处理程序并在打开的监听器上启动它。我们现在准备去处理请求。 客户端的实现也不难,我会说更容易一些: ```go const address = "localhost:50051" func main() { conn, err := grpc.Dial(address, grpc.WithInsecure()) if err != nil { log.Fatalf("did not connect: %v", err) } defer conn.Close() c := pb.NewGravatarServiceClient(conn) ctx, cancel := context.WithTimeout(context.Background(), time.Second) defer cancel() r, err := c.Generate(ctx, &pb.GravatarRequest{Email: "name", Size: 10}) if err != nil { log.Fatalf("could not greet: %v", err) } log.Printf("Greeting: %s", r.Url) } ``` 我们在给定的地址上开启一个特定的连接(在我们的例子中,它就是 `localhost` 与先前定义的端口),我们在给定的连接上注册一个新的客户端。记住,当退出我们的程序时,必须要同时关闭连接并停掉 `context`。 最后,在我们的客户端使用 `GravatarRequest` 调用 `Generate` 方法,同时我们的数据也在里面。如果成功,我们可以使用哈希打印接收到的 URL。 > *订阅立即获取最新内容* > > [*https://tinyletter.com/KamiLelonek*](https://tinyletter.com/KamilLelonek) ## 总结 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` 文件交给使用目标语言编写的代码生成器,你就为这些体系结构之间的安全性和互操作性提供了一些很好的保证。 <div style="text-align: center; font-size: 24px; padding: 1px;">. . .</div> 你可以说你仍然会在一些简单的情况下使用 JSON,我同意,并没有完全的替换 JSON,特别是对于直接由 Web 浏览器使用的服务。我希望你能在自己的用例中为它们找到合适的位置。 ![0_TsWKXNyLzgqchDTh](https://raw.githubusercontent.com/studygolang/gctt-images/master/a-brief-introduction-to-grpc-in-go/0_TsWKXNyLzgqchDTh.png)

via: https://blog.lelonek.me/a-brief-introduction-to-grpc-in-go-e66e596fe244

作者:Kamil Lelonek  译者:PotoYang  校对:polaris1119

本文由 GCTT 原创编译,Go语言中文网 荣誉推出


有疑问加站长微信联系(非本文作者))

入群交流(和以上内容无关):加入Go大咖交流群,或添加微信:liuxiaoyan-s 备注:入群;或加QQ群:692541889

1410 次点击  
加入收藏 微博
被以下专栏收入,发现更多相似内容
暂无回复
添加一条新回复 (您需要 登录 后才能回复 没有账号 ?)
  • 请尽量让自己的回复能够对别人有帮助
  • 支持 Markdown 格式, **粗体**、~~删除线~~、`单行代码`
  • 支持 @ 本站用户;支持表情(输入 : 提示),见 Emoji cheat sheet
  • 图片支持拖拽、截图粘贴等方式上传