KézaKo le gRPC ?
Présentation du protocole gRPC et du “protocole des tampons” soit en Anglais protocol buffers.
Le Protocol buffer permet de définir les structures de données qui seront échangées entre les clients et le service. Il permet aussi de définir les contrats d’interface. Le tout dans un fichier texte simple avec l’extension .proto. Je rentrai un peu plus dans les détails de celui-ci dans une prochaine section.
gRPC est développé par la société Google c’est un Framework permettant de facilité la communication inter-plateforme dans des systèmes distribués.
RPC signifiant Remote Procedure Call.
gRPC utilise le protocole HTTP/2 et le Protocol Buffer pour la description des contrats d’interface.
Premier pas
Dans un application cliente avec gRPC, elle fait appelle directement à une méthode se trouvant dans une application qui se trouve sur un autre serveur de manière complétement transparente pour elle. L’application client pense faire un appel local à cette méthode.
Un des gros avantages est la facilité dans la création d’applications distribuées. Comme dans la technologie RPC, il faut définir l’ensemble des méthodes, des paramètres d’entrées et de retours, on parle de contrat d’interface et de messages.
Côté serveur
Côté serveur, celui-ci implémente le contrat d’interface et le met à disposition au travers d’une application gRPC qui répondra aux demandes des différents clients.
Côté client
Côté client, il ne dispose que du contrat d’interface, on parlera de stub pour effectuer les appels aux différentes méthodes.
Disponibilité
La partie serveur, comme la partie cliente peuvent se trouver dans différents environnements, écrit dans différents langages qui disposent d’une implémentation de gRPC et pouvoir discuter facilement entre eux. On peut timagine un serveur gRPC écrit en C# qui dialogue avec des clients écrit en GO, Java, Ruby etc…
gRPC étant un Framework développé par Google, on retrouvera dans les API de Google des fonctions permettant la création et la consommation de service gRPC.
Architecture
Présentation des échanges entre les clients et le service.
Protocole de Tampons ou Protocol Buffers
Présentation
De base gRPC utilise le Buffer Protocol. J’utiliserai le terme Anglais et donc Protocol Buffers pour parler de cette technologie ce qui sera plus facile pour la compréhension.
Protocol Buffers a été créé par la société Google en 2001. C’est un format de sérialisation des données structurées par un langage de description. A l’origine développé pour le C++, JAVA et Python, il est disponible sur d’autres plateformes.
Grâce à son langage de description il va permettre à des applications distantes de pouvoir échanger des données entre elles. Ce langage de description des données va permettre de définir les différentes structures de données, messages qui seront échangées entre le client et le service.
Il va permettre aussi de définir le contrat de service qui sera disponible pour les clients, le tout, donc dans un fichier texte portant l’extension .proto.
Principe
Il suffira donc au développeur de définir dans un fichier proto l’ensemble des messages et des contrats de service. Ensuite à l’aide d’un transpilateur de traduire dans un langage cible de développement qui servira pour invoqué par le client ou le service pour dialoguer et se transmettre des données.
Les messages seront alors sérialisées au format binaire compacté. En gardant un compatibilité ascendante et descendante. Mais il sera impossible de récupérer les noms des propriétés, types, etc… car il n’est pas autodescriptif.
Exemples
Exemple de fichier proto de définition d’un message et de sa génération en langage GO.
syntax = "proto3"; option go_package = "fredericschmidt.fr/protobuf/sample1"; message SearchRequest { string query = 1; int32 page_number = 2; int32 result_per_page = 3; }
// Code generated by protoc-gen-go. DO NOT EDIT. // versions: // protoc-gen-go v1.25.0-devel // protoc v3.12.3 // source: sample1.proto package sample1 import ( proto "github.com/golang/protobuf/proto" protoreflect "google.golang.org/protobuf/reflect/protoreflect" protoimpl "google.golang.org/protobuf/runtime/protoimpl" reflect "reflect" sync "sync" ) const ( // Verify that this generated code is sufficiently up-to-date. _ = protoimpl.EnforceVersion(20 - protoimpl.MinVersion) // Verify that runtime/protoimpl is sufficiently up-to-date. _ = protoimpl.EnforceVersion(protoimpl.MaxVersion - 20) ) // This is a compile-time assertion that a sufficiently up-to-date version // of the legacy proto package is being used. const _ = proto.ProtoPackageIsVersion4 type SearchRequest struct { state protoimpl.MessageState sizeCache protoimpl.SizeCache unknownFields protoimpl.UnknownFields Query string `protobuf:"bytes,1,opt,name=query,proto3" json:"query,omitempty"` PageNumber int32 `protobuf:"varint,2,opt,name=page_number,json=pageNumber,proto3" json:"page_number,omitempty"` ResultPerPage int32 `protobuf:"varint,3,opt,name=result_per_page,json=resultPerPage,proto3" json:"result_per_page,omitempty"` } func (x *SearchRequest) Reset() { *x = SearchRequest{} if protoimpl.UnsafeEnabled { mi := &file_sample1_proto_msgTypes[0] ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) ms.StoreMessageInfo(mi) } } func (x *SearchRequest) String() string { return protoimpl.X.MessageStringOf(x) } func (*SearchRequest) ProtoMessage() {} func (x *SearchRequest) ProtoReflect() protoreflect.Message { mi := &file_sample1_proto_msgTypes[0] if protoimpl.UnsafeEnabled && x != nil { ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) if ms.LoadMessageInfo() == nil { ms.StoreMessageInfo(mi) } return ms } return mi.MessageOf(x) } // Deprecated: Use SearchRequest.ProtoReflect.Descriptor instead. func (*SearchRequest) Descriptor() ([]byte, []int) { return file_sample1_proto_rawDescGZIP(), []int{0} } func (x *SearchRequest) GetQuery() string { if x != nil { return x.Query } return "" } func (x *SearchRequest) GetPageNumber() int32 { if x != nil { return x.PageNumber } return 0 } func (x *SearchRequest) GetResultPerPage() int32 { if x != nil { return x.ResultPerPage } return 0 } var File_sample1_proto protoreflect.FileDescriptor var file_sample1_proto_rawDesc = []byte{ 0x0a, 0x0d, 0x73, 0x61, 0x6d, 0x70, 0x6c, 0x65, 0x31, 0x2e, 0x70, 0x72, 0x6f, 0x74, 0x6f, 0x22, 0x6e, 0x0a, 0x0d, 0x53, 0x65, 0x61, 0x72, 0x63, 0x68, 0x52, 0x65, 0x71, 0x75, 0x65, 0x73, 0x74, 0x12, 0x14, 0x0a, 0x05, 0x71, 0x75, 0x65, 0x72, 0x79, 0x18, 0x01, 0x20, 0x01, 0x28, 0x09, 0x52, 0x05, 0x71, 0x75, 0x65, 0x72, 0x79, 0x12, 0x1f, 0x0a, 0x0b, 0x70, 0x61, 0x67, 0x65, 0x5f, 0x6e, 0x75, 0x6d, 0x62, 0x65, 0x72, 0x18, 0x02, 0x20, 0x01, 0x28, 0x05, 0x52, 0x0a, 0x70, 0x61, 0x67, 0x65, 0x4e, 0x75, 0x6d, 0x62, 0x65, 0x72, 0x12, 0x26, 0x0a, 0x0f, 0x72, 0x65, 0x73, 0x75, 0x6c, 0x74, 0x5f, 0x70, 0x65, 0x72, 0x5f, 0x70, 0x61, 0x67, 0x65, 0x18, 0x03, 0x20, 0x01, 0x28, 0x05, 0x52, 0x0d, 0x72, 0x65, 0x73, 0x75, 0x6c, 0x74, 0x50, 0x65, 0x72, 0x50, 0x61, 0x67, 0x65, 0x42, 0x25, 0x5a, 0x23, 0x66, 0x72, 0x65, 0x64, 0x65, 0x72, 0x69, 0x63, 0x73, 0x63, 0x68, 0x6d, 0x69, 0x64, 0x74, 0x2e, 0x66, 0x72, 0x2f, 0x70, 0x72, 0x6f, 0x74, 0x6f, 0x62, 0x75, 0x66, 0x2f, 0x73, 0x61, 0x6d, 0x70, 0x6c, 0x65, 0x31, 0x62, 0x06, 0x70, 0x72, 0x6f, 0x74, 0x6f, 0x33, } var ( file_sample1_proto_rawDescOnce sync.Once file_sample1_proto_rawDescData = file_sample1_proto_rawDesc ) func file_sample1_proto_rawDescGZIP() []byte { file_sample1_proto_rawDescOnce.Do(func() { file_sample1_proto_rawDescData = protoimpl.X.CompressGZIP(file_sample1_proto_rawDescData) }) return file_sample1_proto_rawDescData } var file_sample1_proto_msgTypes = make([]protoimpl.MessageInfo, 1) var file_sample1_proto_goTypes = []interface{}{ (*SearchRequest)(nil), // 0: SearchRequest } var file_sample1_proto_depIdxs = []int32{ 0, // [0:0] is the sub-list for method output_type 0, // [0:0] is the sub-list for method input_type 0, // [0:0] is the sub-list for extension type_name 0, // [0:0] is the sub-list for extension extendee 0, // [0:0] is the sub-list for field type_name } func init() { file_sample1_proto_init() } func file_sample1_proto_init() { if File_sample1_proto != nil { return } if !protoimpl.UnsafeEnabled { file_sample1_proto_msgTypes[0].Exporter = func(v interface{}, i int) interface{} { switch v := v.(*SearchRequest); i { case 0: return &v.state case 1: return &v.sizeCache case 2: return &v.unknownFields default: return nil } } } type x struct{} out := protoimpl.TypeBuilder{ File: protoimpl.DescBuilder{ GoPackagePath: reflect.TypeOf(x{}).PkgPath(), RawDescriptor: file_sample1_proto_rawDesc, NumEnums: 0, NumMessages: 1, NumExtensions: 0, NumServices: 0, }, GoTypes: file_sample1_proto_goTypes, DependencyIndexes: file_sample1_proto_depIdxs, MessageInfos: file_sample1_proto_msgTypes, }.Build() File_sample1_proto = out.File file_sample1_proto_rawDesc = nil file_sample1_proto_goTypes = nil file_sample1_proto_depIdxs = nil }
Exemple de fichier proto de définition d’un contrat de service et des messages qui seront échangés à l’appel des différentes méthodes et bien sûr le source en langage GO après transpilation.
syntax = "proto3"; option go_package = "fredericschmidt.fr/protobuf/sample2"; service Greeter { rpc SayHello(HelloRequest) returns (HelloReply) {} } message HelloReply { string message = 1; } message HelloRequest { string message = 1; }
// Code generated by protoc-gen-go. DO NOT EDIT. // versions: // protoc-gen-go v1.25.0-devel // protoc v3.12.3 // source: sample2.proto package sample2 import ( proto "github.com/golang/protobuf/proto" protoreflect "google.golang.org/protobuf/reflect/protoreflect" protoimpl "google.golang.org/protobuf/runtime/protoimpl" reflect "reflect" sync "sync" ) const ( // Verify that this generated code is sufficiently up-to-date. _ = protoimpl.EnforceVersion(20 - protoimpl.MinVersion) // Verify that runtime/protoimpl is sufficiently up-to-date. _ = protoimpl.EnforceVersion(protoimpl.MaxVersion - 20) ) // This is a compile-time assertion that a sufficiently up-to-date version // of the legacy proto package is being used. const _ = proto.ProtoPackageIsVersion4 type HelloReply struct { state protoimpl.MessageState sizeCache protoimpl.SizeCache unknownFields protoimpl.UnknownFields Message string `protobuf:"bytes,1,opt,name=message,proto3" json:"message,omitempty"` } func (x *HelloReply) Reset() { *x = HelloReply{} if protoimpl.UnsafeEnabled { mi := &file_sample2_proto_msgTypes[0] ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) ms.StoreMessageInfo(mi) } } func (x *HelloReply) String() string { return protoimpl.X.MessageStringOf(x) } func (*HelloReply) ProtoMessage() {} func (x *HelloReply) ProtoReflect() protoreflect.Message { mi := &file_sample2_proto_msgTypes[0] if protoimpl.UnsafeEnabled && x != nil { ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) if ms.LoadMessageInfo() == nil { ms.StoreMessageInfo(mi) } return ms } return mi.MessageOf(x) } // Deprecated: Use HelloReply.ProtoReflect.Descriptor instead. func (*HelloReply) Descriptor() ([]byte, []int) { return file_sample2_proto_rawDescGZIP(), []int{0} } func (x *HelloReply) GetMessage() string { if x != nil { return x.Message } return "" } type HelloRequest struct { state protoimpl.MessageState sizeCache protoimpl.SizeCache unknownFields protoimpl.UnknownFields Message string `protobuf:"bytes,1,opt,name=message,proto3" json:"message,omitempty"` } func (x *HelloRequest) Reset() { *x = HelloRequest{} if protoimpl.UnsafeEnabled { mi := &file_sample2_proto_msgTypes[1] ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) ms.StoreMessageInfo(mi) } } func (x *HelloRequest) String() string { return protoimpl.X.MessageStringOf(x) } func (*HelloRequest) ProtoMessage() {} func (x *HelloRequest) ProtoReflect() protoreflect.Message { mi := &file_sample2_proto_msgTypes[1] if protoimpl.UnsafeEnabled && x != nil { ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) if ms.LoadMessageInfo() == nil { ms.StoreMessageInfo(mi) } return ms } return mi.MessageOf(x) } // Deprecated: Use HelloRequest.ProtoReflect.Descriptor instead. func (*HelloRequest) Descriptor() ([]byte, []int) { return file_sample2_proto_rawDescGZIP(), []int{1} } func (x *HelloRequest) GetMessage() string { if x != nil { return x.Message } return "" } var File_sample2_proto protoreflect.FileDescriptor var file_sample2_proto_rawDesc = []byte{ 0x0a, 0x0d, 0x73, 0x61, 0x6d, 0x70, 0x6c, 0x65, 0x32, 0x2e, 0x70, 0x72, 0x6f, 0x74, 0x6f, 0x22, 0x26, 0x0a, 0x0a, 0x48, 0x65, 0x6c, 0x6c, 0x6f, 0x52, 0x65, 0x70, 0x6c, 0x79, 0x12, 0x18, 0x0a, 0x07, 0x6d, 0x65, 0x73, 0x73, 0x61, 0x67, 0x65, 0x18, 0x01, 0x20, 0x01, 0x28, 0x09, 0x52, 0x07, 0x6d, 0x65, 0x73, 0x73, 0x61, 0x67, 0x65, 0x22, 0x28, 0x0a, 0x0c, 0x48, 0x65, 0x6c, 0x6c, 0x6f, 0x52, 0x65, 0x71, 0x75, 0x65, 0x73, 0x74, 0x12, 0x18, 0x0a, 0x07, 0x6d, 0x65, 0x73, 0x73, 0x61, 0x67, 0x65, 0x18, 0x01, 0x20, 0x01, 0x28, 0x09, 0x52, 0x07, 0x6d, 0x65, 0x73, 0x73, 0x61, 0x67, 0x65, 0x32, 0x33, 0x0a, 0x07, 0x47, 0x72, 0x65, 0x65, 0x74, 0x65, 0x72, 0x12, 0x28, 0x0a, 0x08, 0x53, 0x61, 0x79, 0x48, 0x65, 0x6c, 0x6c, 0x6f, 0x12, 0x0d, 0x2e, 0x48, 0x65, 0x6c, 0x6c, 0x6f, 0x52, 0x65, 0x71, 0x75, 0x65, 0x73, 0x74, 0x1a, 0x0b, 0x2e, 0x48, 0x65, 0x6c, 0x6c, 0x6f, 0x52, 0x65, 0x70, 0x6c, 0x79, 0x22, 0x00, 0x42, 0x25, 0x5a, 0x23, 0x66, 0x72, 0x65, 0x64, 0x65, 0x72, 0x69, 0x63, 0x73, 0x63, 0x68, 0x6d, 0x69, 0x64, 0x74, 0x2e, 0x66, 0x72, 0x2f, 0x70, 0x72, 0x6f, 0x74, 0x6f, 0x62, 0x75, 0x66, 0x2f, 0x73, 0x61, 0x6d, 0x70, 0x6c, 0x65, 0x32, 0x62, 0x06, 0x70, 0x72, 0x6f, 0x74, 0x6f, 0x33, } var ( file_sample2_proto_rawDescOnce sync.Once file_sample2_proto_rawDescData = file_sample2_proto_rawDesc ) func file_sample2_proto_rawDescGZIP() []byte { file_sample2_proto_rawDescOnce.Do(func() { file_sample2_proto_rawDescData = protoimpl.X.CompressGZIP(file_sample2_proto_rawDescData) }) return file_sample2_proto_rawDescData } var file_sample2_proto_msgTypes = make([]protoimpl.MessageInfo, 2) var file_sample2_proto_goTypes = []interface{}{ (*HelloReply)(nil), // 0: HelloReply (*HelloRequest)(nil), // 1: HelloRequest } var file_sample2_proto_depIdxs = []int32{ 1, // 0: Greeter.SayHello:input_type -> HelloRequest 0, // 1: Greeter.SayHello:output_type -> HelloReply 1, // [1:2] is the sub-list for method output_type 0, // [0:1] is the sub-list for method input_type 0, // [0:0] is the sub-list for extension type_name 0, // [0:0] is the sub-list for extension extendee 0, // [0:0] is the sub-list for field type_name } func init() { file_sample2_proto_init() } func file_sample2_proto_init() { if File_sample2_proto != nil { return } if !protoimpl.UnsafeEnabled { file_sample2_proto_msgTypes[0].Exporter = func(v interface{}, i int) interface{} { switch v := v.(*HelloReply); i { case 0: return &v.state case 1: return &v.sizeCache case 2: return &v.unknownFields default: return nil } } file_sample2_proto_msgTypes[1].Exporter = func(v interface{}, i int) interface{} { switch v := v.(*HelloRequest); i { case 0: return &v.state case 1: return &v.sizeCache case 2: return &v.unknownFields default: return nil } } } type x struct{} out := protoimpl.TypeBuilder{ File: protoimpl.DescBuilder{ GoPackagePath: reflect.TypeOf(x{}).PkgPath(), RawDescriptor: file_sample2_proto_rawDesc, NumEnums: 0, NumMessages: 2, NumExtensions: 0, NumServices: 1, }, GoTypes: file_sample2_proto_goTypes, DependencyIndexes: file_sample2_proto_depIdxs, MessageInfos: file_sample2_proto_msgTypes, }.Build() File_sample2_proto = out.File file_sample2_proto_rawDesc = nil file_sample2_proto_goTypes = nil file_sample2_proto_depIdxs = nil }
Les exemples de fichiers proto sont dans la version comme vous pourrez le constater dans sa première instruction.
Je n’entrerai pas dans les détails du protocol buffers car cela sortirait du cadre et du sujet principal de ce billet.
Je n’entrerai pas non plus dans les détails du langage GO. Je fais des billets sur ce langage.
Outils
Pour la transpilation du fichier proto vers le langage de développement cible j’ai utilisé l’utilitaire protoc qui est disponible sur le site de Google concernant le protocol buffers.
Cette outils ne généré pas en langage GO directement il faut utiliser un autre utilitaire développer aussi, mais pas que, par Google qui sera appelé par protoc lors de la génération.
Cet utilitaire ce nomme protoc-gen-go pour celui développé par Google.
Résumé
Nous venons de faire notre première incursion dans le domaine du gRPC de Google* et aussi de manière très très légère sur le Protocol buffers.
Le prochaine billet se concentrera sur le Core de gRPC, avec un exemple concret.
Références
Voici quelques références sur le sujet gRPC et Protocol Buffers de Google.
Site Officiel du Protocol Buffers
GitHub de l’extension de Google pour transpiler en langage GO
Ajouer un commentaire