Questo tutorial fornisce un’introduzione di base del programmatore Dart al lavoro con i buffer di protocollo, utilizzando la versione proto3 del linguaggio protocol buffer. Camminando attraverso la creazione di una semplice applicazione di esempio, ti mostra come
- Definire i formati dei messaggi in un file
.proto
. - Utilizzare il compilatore buffer di protocollo.
- Usa l’API Dart protocol buffer per scrivere e leggere messaggi.
Questa non è una guida completa all’utilizzo dei buffer di protocollo in Dart . Per informazioni di riferimento più dettagliate, consultare la Guida al linguaggio buffer del protocollo, il Tour della lingua Dart, il riferimento API Dart, la Guida al codice generato Dart e il riferimento alla codifica.
Perché usare i buffer di protocollo?
L’esempio che useremo è un’applicazione molto semplice “rubrica” in grado di leggere e scrivere i dettagli di contatto delle persone da e verso un file. Ogni persona nella rubrica ha un nome, un ID, un indirizzo e-mail e un numero di telefono di contatto.
Come si fa a serializzare e recuperare dati strutturati come questo? Ci sono alcuni modi per risolvere questo problema:
- Puoi inventare un modo ad hoc per codificare gli elementi di dati in una singola stringa, come la codifica di 4 int come “12:3:-23:67”. Questo è un approccio semplice e flessibile, anche se richiede la scrittura di codifica una tantum e codice di analisi, e l’analisi impone un piccolo costo di runtime. Questo funziona meglio per la codifica di dati molto semplici.
- Serializza i dati in XML. Questo approccio può essere molto interessante poiché XML è (una sorta di) leggibile dall’uomo e ci sono librerie vincolanti per molte lingue. Questa può essere una buona scelta se si desidera condividere i dati con altre applicazioni/progetti. Tuttavia, XML è notoriamente ad alta intensità di spazio e codifica/decodifica può imporre un’enorme penalizzazione delle prestazioni alle applicazioni. Inoltre, la navigazione di un albero DOM XML è notevolmente più complicata di quanto sarebbe normalmente la navigazione di campi semplici in una classe.
I buffer di protocollo sono la soluzione flessibile, efficiente e automatizzata per risolvere esattamente questo problema. Con i buffer di protocollo, si scrive una descrizione.proto
della struttura dati che si desidera memorizzare. Da ciò, il compilatore del buffer di protocollo crea una classe che implementa la codifica automatica e l’analisi dei dati del buffer di protocollo con un formato binario efficiente. La classe generata fornisce getter e setter per i campi che compongono un buffer di protocollo e si occupa dei dettagli di lettura e scrittura del buffer di protocollo come unità. È importante sottolineare che il formato buffer del protocollo supporta l’idea di estendere il formato nel tempo in modo tale che il codice possa ancora leggere i dati codificati con il vecchio formato.
Dove trovare il codice di esempio
Il nostro esempio è un insieme di riga di comandoapplicazioni per la gestione di una rubrica di indirizzofile di dati, codificato utilizzando buffer di protocollo.Il comando dart add_person.dart
aggiunge una nuova voce al file di dati. Il comandodart list_people.dart
analizza il file di datie stampa i dati nella console.
È possibile trovare l’esempio completo nell’examples directoryof del repository GitHub.
Definizione del formato del protocollo
Per creare l’applicazione della rubrica, è necessario iniziare con un file.proto
. Le definizioni in un file.proto
sono semplici: si aggiunge un messaggio per ogni struttura di dati che si desidera serializzare, quindi si specifica un nome e un tipo per ogni campo nel messaggio. Nel nostro esempio, il file.proto
che definisce i messaggi èaddressbook.proto
.
Il file .proto
inizia con una dichiarazione del pacchetto, che aiutaper prevenire conflitti di denominazione tra diversi progetti.
syntax = "proto3";package tutorial;import "google/protobuf/timestamp.proto";
Successivamente, hai le definizioni dei messaggi. Un messaggio è solo un aggregatocontenendo un insieme di campi digitati. Molti tipi di dati semplici areavailable come i tipi di campo, tra cui bool
int32
float
double
e string
. Puoi anche aggiungere ulteriore struttura ai tuoi messaggi utilizzando altri tipi di messaggi come tipi di campo.
message Person { string name = 1; int32 id = 2; // Unique ID number for this person. string email = 3; enum PhoneType { MOBILE = 0; HOME = 1; WORK = 2; } message PhoneNumber { string number = 1; PhoneType type = 2; } repeated PhoneNumber phones = 4; google.protobuf.Timestamp last_updated = 5;}// Our address book file is just one of these.message AddressBook { repeated Person people = 1;}
Nell’esempio precedente, il Person
messaggio contienePhoneNumber
messaggi, mentre il AddressBook
messagecontains Person
messaggi. È anche possibile definire i tipi di messaggionestati all’interno di altri messaggi – come puoi vedere, il tipoPhoneNumber
è definito all’interno di Person
. Puoi anche definire i tipi enum
se vuoi che uno dei tuoi campi abbia un elenco predefinito di valori – qui vuoi specificare che un numero di telefono può essere uno di MOBILE
HOME
, oWORK
.
I marcatori “= 1”, “= 2” su ciascun elemento identificano il “tag” univoco che il campo utilizza nella codifica binaria. I numeri di tag 1-15 richiedono un byte in meno per codificare rispetto ai numeri più alti, quindi come ottimizzazione puoi decidere di utilizzare quei tag per gli elementi comunemente usati o ripetuti, lasciando i tag 16 e superiori per gli elementi opzionali meno comunemente usati. Ogni elemento in un campo ripetuto richiede la ricodifica del numero di tag, quindi i campi ripetuti sono candidati particolarmente validi per questa ottimizzazione.
Se un valore di campo non è impostato, viene utilizzato un valore predefinito: zeroper i tipi numerici, la stringa vuota per le stringhe, false per i bool. Per embeddedmessages, il valore predefinito è sempre “istanza predefinita” o “prototipo” del messaggio, che non ha nessuno dei suoi campi impostati. Chiamare l’accessor per ottenere il valore di un campo che non è stato impostato esplicitamente restituisce sempre il valore predefinito di quel campo.
Se un campo è repeated
, il campo può essere ripetuto qualsiasi numero di volte (incluso zero). L’ordine dei valori ripetuti verrà conservato nel buffer del protocollo. Pensa ai campi ripetuti come array di dimensioni dinamiche.
Troverai una guida completa alla scrittura di file.proto
, inclusi tutti i possibili tipi di campo, nella Guida al linguaggio buffer del protocollo. Non cercare strutture simili all’ereditarietà della classe, però – i buffer del protocollo non lo fanno.
la Compilazione del protocollo di buffer
Ora che avete un .proto
, la prossima cosa che dovete fare è generare le classi avrete bisogno di leggere e scrivere AddressBook
(e quindi Person
e PhoneNumber
messaggi. Per fare ciò, è necessario eseguire il compilatore del buffer di protocollo protoc
sul .proto
:
- Se non è stato installato il compilatore, scaricare il pacchetto e seguire le istruzioni nel README.
- Installa il plugin Dart Protocol Buffer come descritto nel suo README. L’eseguibile
bin/protoc-gen-dart
deve essere nel tuoPATH
per il buffer del protocolloprotoc
per trovarlo. - Ora esegui il compilatore, specificando la directory sorgente (dove vive il codice sorgente dell’applicazione – la directory corrente viene utilizzata se non fornisci un valore), la directory di destinazione (dove vuoi che vada il codice generato; spesso lo stesso di
$SRC_DIR
) e il percorso del tuo.proto
. In questo caso, si invocherà:protoc -I=$SRC_DIR --dart_out=$DST_DIR $SRC_DIR/addressbook.proto
Poiché si desidera il codice Dart, si utilizza l’opzione
--dart_out
– opzioni simili sono fornite per altre lingue supportate.
Questo generaaddressbook.pb.dart
nella directory di destinazione specificata.
L’API del buffer di protocollo
Che generaaddressbook.pb.dart
fornisce i seguenti tipi utili:
- An
AddressBook
classe con unList<Person> get people
getter. - Un
Person
classe con metodi supplementari pername
id
email
ephones
. - A
Person_PhoneNumber
classe, con metodi di accesso pernumber
etype
. - A
Person_PhoneType
classe con campi statici per ogni valore nell’enumerazionePerson.PhoneType
.
Puoi leggere di più sui dettagli di esattamente ciò che viene generato nella guida al codice generato da Dart.
Scrivere un messaggio
Ora proviamo a usare le classi buffer del protocollo. La prima cosa che vuoi che la tua applicazione di rubrica sia in grado di fare è scrivere dettagli personali sul tuo file di rubrica. Per fare ciò, è necessario creare e popolare le istanze delle classi buffer del protocollo e quindi scriverle in un flusso di output.
Ecco un programma che legge unAddressBook
da un file, aggiunge un nuovoPerson
in base all’input dell’utente e scrive nuovamente il nuovoAddressBook
nel file. Vengono evidenziate le parti che chiamano direttamente o fanno riferimento al codice generato dal compilatore del protocollo.
import 'dart:io';import 'dart_tutorial/addressbook.pb.dart';// This function fills in a Person message based on user input.Person promtForAddress() { Person person = Person(); print('Enter person ID: '); String input = stdin.readLineSync(); person.id = int.parse(input); print('Enter name'); person.name = stdin.readLineSync(); print('Enter email address (blank for none) : '); String email = stdin.readLineSync(); if (email.isNotEmpty) { person.email = email; } while (true) { print('Enter a phone number (or leave blank to finish): '); String number = stdin.readLineSync(); if (number.isEmpty) break; Person_PhoneNumber phoneNumber = Person_PhoneNumber(); phoneNumber.number = number; print('Is this a mobile, home, or work phone? '); String type = stdin.readLineSync(); switch (type) { case 'mobile': phoneNumber.type = Person_PhoneType.MOBILE; break; case 'home': phoneNumber.type = Person_PhoneType.HOME; break; case 'work': phoneNumber.type = Person_PhoneType.WORK; break; default: print('Unknown phone type. Using default.'); } person.phones.add(phoneNumber); } return person;}// Reads the entire address book from a file, adds one person based// on user input, then writes it back out to the same file.main(List arguments) { if (arguments.length != 1) { print('Usage: add_person ADDRESS_BOOK_FILE'); exit(-1); } File file = File(arguments.first); AddressBook addressBook; if (!file.existsSync()) { print('File not found. Creating new file.'); addressBook = AddressBook(); } else { addressBook = AddressBook.fromBuffer(file.readAsBytesSync()); } addressBook.people.add(promtForAddress()); file.writeAsBytes(addressBook.writeToBuffer());}
Leggere un messaggio
Naturalmente, una rubrica non sarebbe molto utile se non si potesse ottenere alcuna informazione fuori di esso! Questo esempio legge il file creato dall’esempio precedente e stampa tutte le informazioni in esso contenute.
import 'dart:io';import 'dart_tutorial/addressbook.pb.dart';import 'dart_tutorial/addressbook.pbenum.dart';// Iterates though all people in the AddressBook and prints info about them.void printAddressBook(AddressBook addressBook) { for (Person person in addressBook.people) { print('Person ID: ${ person.id}'); print(' Name: ${ person.name}'); if (person.hasEmail()) { print(' E-mail address:${ person.email}'); } for (Person_PhoneNumber phoneNumber in person.phones) { switch (phoneNumber.type) { case Person_PhoneType.MOBILE: print(' Mobile phone #: '); break; case Person_PhoneType.HOME: print(' Home phone #: '); break; case Person_PhoneType.WORK: print(' Work phone #: '); break; default: print(' Unknown phone #: '); break; } print(phoneNumber.number); } }}// Reads the entire address book from a file and prints all// the information inside.main(List arguments) { if (arguments.length != 1) { print('Usage: list_person ADDRESS_BOOK_FILE'); exit(-1); } // Read the existing address book. File file = new File(arguments.first); AddressBook addressBook = new AddressBook.fromBuffer(file.readAsBytesSync()); printAddressBook(addressBook);}
Estendere un buffer di protocollo
Prima o poi,dopo aver rilasciato il codice che utilizza il buffer di protocollo, si vorrà senza dubbio “migliorare” la definizione del buffer di protocollo. Se vuoi che i tuoi nuovi buffer siano retrocompatibili e che i tuoi vecchi buffer siano compatibili con beforward-e quasi certamente lo vuoi-allora ci sono alcune regole che devi seguire. Nella nuova versione del buffer protocol:
- non è necessario modificare i numeri di tag di alcun campo esistente.
- è possibile eliminare i campi.
- puoi aggiungere nuovi campi ma devi usare nuovi numeri di tag (cioè numeri di tag che non sono mai stati usati in questo buffer di protocollo, nemmeno dai campi eliminati).
(Ci sonoalcune eccezioni a queste regole, ma sono usate raramente.)
Se segui queste regole, il vecchio codice leggerà felicemente nuovi messaggi e ignorerà semplicemente tutti i nuovi campi. Al vecchio codice, i campi singolari che sono stati eliminati avranno semplicemente il loro valore predefinito e i campi ripetuti eliminati saranno vuoti. Il nuovo codice leggerà anche in modo trasparente i vecchi messaggi.
Tuttavia, tieni presente che i nuovi campi non saranno presenti nei vecchi messaggi,quindi dovrai fare qualcosa di ragionevole con il valore predefinito. Atype-specificdefault valueis used: per le stringhe, il valore predefinito è la stringa vuota. Per i booleani, il valore predefinito è false. Per i tipi numerici, il valore predefinito è zero.