protocol buffer e libra

Protocol buffers e gRPC

Protocol Buffesr o protobuff per gli amici è un protocollo versatile per serializzare dati strutturati e definire servizi RPC.

Perchè protocol buffers è versatile?

  • E’ indipendente dai linguaggi di programmazione.
  • E’ indipendente dalle piattaforme.
  • Può essere utilizzato nei protocolli di comunicazione, come nei data storage.
  • E’ molto ben documentato.
message Person {
  string name = 1;
  int32 id = 2;
  optional string email = 3;

  enum PhoneType {
    MOBILE = 0;
    HOME = 1;
    WORK = 2;
  }

  message PhoneNumber {
    string number = 1;
    optional PhoneType type = 2 [default = HOME];
  }

  repeated PhoneNumber phone = 4;
}

Come potete vedere la sintassi è molto semplice. Andiamo per gradi.
Con “message Person { ..}” definiamo un tipo di messaggio chiamato Person.
string name = 1 è uno dei quattro field appartenenti al messaggio.
Possiamo usare le keyword optional se vogliamo renderlo opzionale, oppure repeated se vogliamo ripeterlo da 0 a molte volte.
String indica che il tipo di field è una stringa, name è il nome del field, mentre “= 1” non sta ad indicare che il field ha valore 1, ma in protobuff ogni definizione dei field deve avere un numero univoco, identificativo.
la documentazione ufficiale dice:

As you can see, each field in the message definition has a unique number. These field numbers are used to identify your fields in the message binary format, and should not be changed once your message type is in use. 

Per quanto riguarda tutti i tipi di variabile, con la relativa conversione nel linguaggio scelto, la Documentazione ufficiale ci viene incontro con una tabella ben dettagliata.

I messaggi sono niente senza RPC (Remote Procedure Call)

Per utilizzare i messaggi definiti in precedenza attraverso un sistema RPC è necessario definire l’interfaccia di un servizio in un .proto file.

service SearchService {
  rpc Search (SearchRequest) returns (SearchResponse);
}

Protocol Buffer compilerà il file .proto e genererà il codice dell’interfaccia e degli stub (nel linguaggio da te scelto. Un esempio più completo, preso da qua:

syntax = "proto3";

service FooService {
  rpc GetFoo (GetFooRequest) returns (GetFooResponse);
}
message GetFooRequest {
  string id = 1;
}
message GetFooResponse {
  string fooName = 1;
  int64 fooValue = 2;
}

gRPC ..wut?

gRPC

gRPC può usare i Protocol Buffer sia per le definizioni delle interfacce ( i service), sia come formato di interscambio di messaggi (i message).

Installazione

Prima di tutto è necessario creare un virtualenv di python 3.6, per chi ha mkvirtuaenv:

$ mkvirtualenv grpc_test --python=/usr/bin/python3.6

altrimenti:

$ python -m pip install virtualenv
$ virtualenv grpc_test -p=python3.6
$ source venv/bin/activate
$ python -m pip install --upgrade pip

Successivamente attivate il virtualenv e poi:

$ pip install grpcio
$ pip install grpcio-tools

Salviamo il file di prima con il nome example.proto e lanciamo il seguente comando:

$ python -m grpc_tools.protoc -I. --python_out=. --grpc_python_out=. example.proto

l’output saranno due file python. Il primo example_pb2.py

# -*- coding: utf-8 -*-
# Generated by the protocol buffer compiler.  DO NOT EDIT!
# source: example.proto

import sys
_b=sys.version_info[0]<3 and (lambda x:x) or (lambda x:x.encode('latin1'))
from google.protobuf import descriptor as _descriptor
from google.protobuf import message as _message
from google.protobuf import reflection as _reflection
from google.protobuf import symbol_database as _symbol_database
# @@protoc_insertion_point(imports)

_sym_db = _symbol_database.Default()




DESCRIPTOR = _descriptor.FileDescriptor(
  name='example.proto',
  package='',
  syntax='proto3',
  serialized_options=None,
  serialized_pb=_b('\n\rexample.proto\"\x1b\n\rGetFooRequest\x12\n\n\x02id\x18\x01 \x01(\t\"3\n\x0eGetFooResponse\x12\x0f\n\x07\x66ooName\x18\x01 \x01(\t\x12\x10\n\x08\x66ooValue\x18\x02 \x01(\x03\x32\x37\n\nFooService\x12)\n\x06GetFoo\x12\x0e.GetFooRequest\x1a\x0f.GetFooResponseb\x06proto3')
)




_GETFOOREQUEST = _descriptor.Descriptor(
  name='GetFooRequest',
  full_name='GetFooRequest',
  filename=None,
  file=DESCRIPTOR,
  containing_type=None,
  fields=[
    _descriptor.FieldDescriptor(
      name='id', full_name='GetFooRequest.id', index=0,
      number=1, type=9, cpp_type=9, label=1,
      has_default_value=False, default_value=_b("").decode('utf-8'),
      message_type=None, enum_type=None, containing_type=None,
      is_extension=False, extension_scope=None,
      serialized_options=None, file=DESCRIPTOR),
  ],
  extensions=[
  ],
  nested_types=[],
  enum_types=[
  ],
  serialized_options=None,
  is_extendable=False,
  syntax='proto3',
  extension_ranges=[],
  oneofs=[
  ],
  serialized_start=17,
  serialized_end=44,
)


_GETFOORESPONSE = _descriptor.Descriptor(
  name='GetFooResponse',
  full_name='GetFooResponse',
  filename=None,
  file=DESCRIPTOR,
  containing_type=None,
  fields=[
    _descriptor.FieldDescriptor(
      name='fooName', full_name='GetFooResponse.fooName', index=0,
      number=1, type=9, cpp_type=9, label=1,
      has_default_value=False, default_value=_b("").decode('utf-8'),
      message_type=None, enum_type=None, containing_type=None,
      is_extension=False, extension_scope=None,
      serialized_options=None, file=DESCRIPTOR),
    _descriptor.FieldDescriptor(
      name='fooValue', full_name='GetFooResponse.fooValue', index=1,
      number=2, type=3, cpp_type=2, label=1,
      has_default_value=False, default_value=0,
      message_type=None, enum_type=None, containing_type=None,
      is_extension=False, extension_scope=None,
      serialized_options=None, file=DESCRIPTOR),
  ],
  extensions=[
  ],
  nested_types=[],
  enum_types=[
  ],
  serialized_options=None,
  is_extendable=False,
  syntax='proto3',
  extension_ranges=[],
  oneofs=[
  ],
  serialized_start=46,
  serialized_end=97,
)

DESCRIPTOR.message_types_by_name['GetFooRequest'] = _GETFOOREQUEST
DESCRIPTOR.message_types_by_name['GetFooResponse'] = _GETFOORESPONSE
_sym_db.RegisterFileDescriptor(DESCRIPTOR)

GetFooRequest = _reflection.GeneratedProtocolMessageType('GetFooRequest', (_message.Message,), dict(
  DESCRIPTOR = _GETFOOREQUEST,
  __module__ = 'example_pb2'
  # @@protoc_insertion_point(class_scope:GetFooRequest)
  ))
_sym_db.RegisterMessage(GetFooRequest)

GetFooResponse = _reflection.GeneratedProtocolMessageType('GetFooResponse', (_message.Message,), dict(
  DESCRIPTOR = _GETFOORESPONSE,
  __module__ = 'example_pb2'
  # @@protoc_insertion_point(class_scope:GetFooResponse)
  ))
_sym_db.RegisterMessage(GetFooResponse)



_FOOSERVICE = _descriptor.ServiceDescriptor(
  name='FooService',
  full_name='FooService',
  file=DESCRIPTOR,
  index=0,
  serialized_options=None,
  serialized_start=99,
  serialized_end=154,
  methods=[
  _descriptor.MethodDescriptor(
    name='GetFoo',
    full_name='FooService.GetFoo',
    index=0,
    containing_service=None,
    input_type=_GETFOOREQUEST,
    output_type=_GETFOORESPONSE,
    serialized_options=None,
  ),
])
_sym_db.RegisterServiceDescriptor(_FOOSERVICE)

DESCRIPTOR.services_by_name['FooService'] = _FOOSERVICE

# @@protoc_insertion_point(module_scope)

il secondo example_pb2_grpc.py

# Generated by the gRPC Python protocol compiler plugin. DO NOT EDIT!
import grpc

import example_pb2 as example__pb2


class FooServiceStub(object):
  # missing associated documentation comment in .proto file
  pass

  def __init__(self, channel):
    """Constructor.

    Args:
      channel: A grpc.Channel.
    """
    self.GetFoo = channel.unary_unary(
        '/FooService/GetFoo',
        request_serializer=example__pb2.GetFooRequest.SerializeToString,
        response_deserializer=example__pb2.GetFooResponse.FromString,
        )


class FooServiceServicer(object):
  # missing associated documentation comment in .proto file
  pass

  def GetFoo(self, request, context):
    # missing associated documentation comment in .proto file
    pass
    context.set_code(grpc.StatusCode.UNIMPLEMENTED)
    context.set_details('Method not implemented!')
    raise NotImplementedError('Method not implemented!')


def add_FooServiceServicer_to_server(servicer, server):
  rpc_method_handlers = {
      'GetFoo': grpc.unary_unary_rpc_method_handler(
          servicer.GetFoo,
          request_deserializer=example__pb2.GetFooRequest.FromString,
          response_serializer=example__pb2.GetFooResponse.SerializeToString,
      ),
  }
  generic_handler = grpc.method_handlers_generic_handler(
      'FooService', rpc_method_handlers)
  server.add_generic_rpc_handlers((generic_handler,))

Adesso implementiamo il server my_server.py:

from concurrent import futures
import time
import logging

import grpc

import example_pb2
import example_pb2_grpc

_ONE_DAY_IN_SECONDS = 60 * 60 * 24


class FooService(example_pb2_grpc.FooServiceServicer):

    def GetFoo(self, request, context):
        return example_pb2.GetFooResponse(fooName='Hello, %s!' % request.id)


def serve():
    server = grpc.server(futures.ThreadPoolExecutor(max_workers=10))
    example_pb2_grpc.add_FooServiceServicer_to_server(FooService(), server)
    server.add_insecure_port('[::]:50051')
    server.start()
    try:
        while True:
            time.sleep(_ONE_DAY_IN_SECONDS)
    except KeyboardInterrupt:
        server.stop(0)


if __name__ == '__main__':
    logging.basicConfig()
    serve()

Da notare il fatto che
la definizione nel nostro Proto file è passata da;

service FooService {
  rpc GetFoo (GetFooRequest) returns (GetFooResponse);
}

a così in Python:

class FooService(example_pb2_grpc.FooServiceServicer):

    def GetFoo(self, request, context):
        return example_pb2.GetFooResponse(fooName='Hello, %s!' % request.id)

il parametro request in ingresso alla GetFoo non è altro che un’istanza della classe GetFooRequest. Infatti accediamo al parametro id.

Il client sarà my_client.py:

 
from __future__ import print_function
import logging

import grpc

import example_pb2
import example_pb2_grpc


def run():
    with grpc.insecure_channel('localhost:50051') as channel:
        stub = example_pb2_grpc.FooServiceStub(channel)
        response = stub.GetFoo(example_pb2.GetFooRequest(id='ciao ale'))
    print("Greeter client received: " + response.fooName)


if __name__ == '__main__':
    logging.basicConfig()
    run()

Per far funzionare il tutto, basterà lanciare prima my_server.py e successivamente my_client.py e l’output che ottenendo questo output:

$ Greeter client received: Hello, ciao ale!

Perchè questa guida? Perchè Libra ha tutti i servizi definiti in .proto files ed un codice in Rust.