Série : gRPC

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.

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.

Référence https://grpc.io/

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 gRPC

Site Officiel du Protocol Buffers

GitHub du transpilateur

GitHub de l’extension de Google pour transpiler en langage GO

Billets sur le langage GO

Frédéric Schmidt

Lead Technical Architect.

Ajouer un commentaire

Laisser un commentaire

Ce site utilise Akismet pour réduire les indésirables. En savoir plus sur comment les données de vos commentaires sont utilisées.

Recent Comments

    %d blogueurs aiment cette page :