Protobuf
Situs Web Resmi: Protocol Buffers | Google Developers
Pengenalan
Tutorial Resmi: Protocol Buffer Basics: Go | Protocol Buffers | Google Developers
Protocol Buffers adalah mekanisme serialisasi data terstruktur yang dapat diperluas, tidak bergantung pada bahasa, tidak bergantung pada protokol, yang di-open-source oleh Google pada tahun 2008. Ini menyediakan unpacking dan packing yang lebih cepat, umum digunakan dalam bidang komunikasi RPC. Ini dapat mendefinisikan cara terstruktur data, dan kemudian menggunakan kode sumber yang dihasilkan khusus untuk dengan mudah menulis dan membaca data terstruktur dari berbagai aliran data, dan menggunakannya dalam berbagai bahasa. Protocol Buffers disebut sebagai protobuf dalam teks berikut.
protobuf cukup populer, terutama dalam ekosistem Go, di mana gRPC menggunakannya sebagai mekanisme serialisasi untuk transmisi protokol.
Sintaks
Pertama, mari kita lihat contoh untuk melihat seperti apa file protobuf secara umum. Secara keseluruhan, sintaksnya sangat sederhana dan dapat dikuasai dalam sekitar sepuluh menit. Di bawah ini adalah contoh file bernama search.proto. Ekstensi file untuk protobuf adalah .proto.
syntax = "proto3";
message SearchRequest {
string query = 1;
string number = 2;
}
message SearchResult {
string data = 1;
}
service SearchService {
rpc Search(SearchRequest) returns(SearchResult);
}- Baris pertama
syntax = "proto3";menunjukkan penggunaan sintaksproto3, yang merupakan default. - Deklarasi
messagemirip dengan struct dan merupakan struktur dasar diproto. SearchRequestmendefinisikan tiga field, masing-masing dengan nama dan tipe.servicemendefinisikan layanan, yang berisi satu atau lebih antarmuka RPC.- Antarmuka RPC harus memiliki tepat satu parameter dan satu nilai kembali, dan tipenya harus
message, bukan tipe dasar.
Selain itu, perhatikan bahwa setiap baris dalam file proto harus diakhiri dengan titik koma.
Komentar
Gaya komentar persis sama dengan Go.
syntax = "proto3";
/* Komentar
* Komentar */
message SearchRequest {
string query = 1; // Komentar
string number = 2;
}Tipe
Modifier tipe hanya dapat muncul di message dan tidak dapat muncul sendiri.
Tipe Dasar
| proto Type | Go Type |
|---|---|
| double | float64 |
| float | float32 |
| int32 | int32 |
| int64 | int64 |
| uint32 | uint32 |
| uint64 | uint64 |
| sint32 | int32 |
| sint64 | int64 |
| fixed32 | uint32 |
| fixed64 | uint64 |
| sfixed32 | int32 |
| sfixed64 | int64 |
| bool | bool |
| string | string |
| bytes | []byte |
Array
Menambahkan modifier repeated sebelum tipe dasar menunjukkan tipe array, sesuai dengan slice di Go.
message Company {
repeated string employee = 1;
}Map
Format untuk mendefinisikan tipe map di protobuf adalah sebagai berikut:
map<key_type, value_type> map_field = N;key_type harus numerik atau string, dan value_type tidak memiliki batasan tipe. Berikut adalah contohnya:
message Person {
map<string, int64> cards = 1;
}Field
Faktanya, proto bukan tipe key-value tradisional. Dalam file proto yang dideklarasikan, data spesifik tidak muncul. Setelah = setiap field, harus ada nomor unik dalam message saat ini. Nomor-nomor ini digunakan untuk mengidentifikasi dan mendefinisikan field-field ini dalam pesan biner. Nomor dimulai dari 1, dengan nomor 1-15 menempati 1 byte, dan 16-2047 menempati 2 byte. Oleh karena itu, field yang sering muncul harus diberi nomor 1-15 untuk menghemat ruang, dan beberapa ruang harus disisakan untuk field yang mungkin sering muncul di masa mendatang.
Field dalam message harus mengikuti aturan ini:
singular: Ini adalah tipe field default. Dalammessageyang terstruktur dengan baik, hanya boleh ada 0 atau 1 field ini, artinya field yang sama tidak dapat ada berulang kali. Deklarasi berikut akan melaporkan error.protobufsyntax = "proto3"; message SearchRequest { string query = 1; string number = 2; string number = 3; // Field duplikat }optional: Mirip dengansingular, tetapi memungkinkan pemeriksaan eksplisit apakah nilai field telah diatur. Mungkin ada dua kasus berikut:set: Akan diserialisasiunset: Tidak akan diserialisasi
repeated: Tipe field ini dapat muncul 0 atau beberapa kali. Nilai berulang akan dipertahankan dalam urutan (sederhananya, ini adalah array yang memungkinkan tipe nilai yang sama muncul beberapa kali dan mempertahankannya dalam urutan kemunculannya, yaitu indeks).map: Field tipe pasangan key-value, dideklarasikan sebagai berikut:protobufmap<string,int32> config = 3;
Field yang Direservasi
Kata kunci reserve dapat mendeklarasikan field yang direservasi. Setelah mendeklarasikan nomor field yang direservasi, itu tidak dapat digunakan sebagai nomor dan nama field lain, dan kompilasi juga akan gagal. Jawaban resmi Google adalah: jika file proto menghapus beberapa nomor dalam versi baru, pengguna lain mungkin menggunakan kembali nomor yang dihapus ini di masa mendatang. Namun, jika kembali ke nomor versi lama, itu akan menyebabkan inkonsistensi antara field dan nomor yang sesuai, menghasilkan error. Field yang direservasi dapat berfungsi sebagai pengingat pada waktu kompilasi, mengingatkan Anda bahwa Anda tidak dapat menggunakan field yang direservasi ini, jika tidak kompilasi akan gagal.
syntax = "proto3";
message SearchRequest {
string query = 1;
string number = 2;
map<string, int32> config = 3;
repeated string a = 4;
reserved "a"; // Deklarasikan field dengan nama spesifik sebagai field yang direservasi
reserved 1 to 2; // Deklarasikan urutan nomor sebagai field yang direservasi
reserved 3,4; // Deklarasikan
}Dengan ini, file tidak akan lulus kompilasi.
Field yang Usang
Jika field usang (deprecated), dapat ditulis sebagai berikut:
message Body {
string name = 1 [deprecated = true];
}Enum
Anda dapat mendeklarasikan konstanta enum dan menggunakannya sebagai tipe field. Perhatikan bahwa elemen pertama enum harus nol karena nilai default enum adalah elemen pertama.
syntax = "proto3";
enum Type {
GET = 0;
POST = 1;
PUT = 2;
DELETE = 3;
}
message SearchRequest {
string query = 1;
string number = 2;
map<string, int32> config = 3;
repeated string a = 4;
Type type = 5;
}Ketika ada item enum dengan nilai yang sama di dalam enum, Anda dapat menggunakan alias enum:
syntax = "proto3";
enum Type {
option allow_alias = true; // Perlu mengaktifkan konfigurasi alias
GET = 0;
GET_ALIAS = 0; // Alias untuk item enum GET
POST = 1;
PUT = 2;
DELETE = 3;
}
message SearchRequest {
string query = 1;
string number = 2;
map<string, int32> config = 3;
repeated string a = 4;
Type type = 5;
}Pesan Bersarang
message Outer { // Level 0
message MiddleAA { // Level 1
message Inner { // Level 2
int64 ival = 1;
bool booly = 2;
}
}
message MiddleBB { // Level 1
message Inner { // Level 2
int32 ival = 1;
bool booly = 2;
}
}
}message dapat menyarangkan deklarasi message, seperti struct bersarang.
Package
Anda dapat menambahkan modifier package opsional ke file protobuf untuk mencegah konflik nama antara tipe pesan protokol.
package foo.bar;
message Open { ... }Kemudian Anda dapat menggunakan nama package saat mendefinisikan field dalam tipe pesan:
message Foo {
...
foo.bar.Open open = 1;
...
}Import
Mengimpor memungkinkan beberapa file protobuf berbagi definisi. Sintaksnya adalah sebagai berikut, dan ekstensi file tidak dapat dihilangkan saat mengimpor.
import "a/b/c.proto";Impor menggunakan path relatif, tetapi path relatif ini bukan path relatif antara file yang mengimpor dan file yang diimpor. Ini tergantung pada path pemindaian yang ditentukan saat compiler protoc menghasilkan kode. Misalkan ada struktur file berikut:
pb_learn
│ common.proto
│
├─monster
│ monster.proto
│
└─player
health.proto
player.protoJika kami hanya perlu menghasilkan kode untuk bagian direktori player dan hanya menentukan direktori player saat memindai path, maka impor timbal balik antara health.proto dan player.proto dapat langsung menulis nama file tunggal. Misalnya, player.proto mengimpor health.proto:
import "health.proto";Jika pada saat ini player.proto mengimpor file di direktori common.proto atau monster, kompilasi akan gagal, jadi penulisan berikut sepenuhnya salah karena compiler tidak dapat menemukan file-file ini:
import "../common.proto"; // Penulisan yang salahTIP
Ngomong-ngomong, simbol .., . tidak diizinkan muncul di path impor.
Misalkan pb_learn ditentukan sebagai path pemindaian selama kompilasi, maka Anda dapat mengimpor file dari direktori lain melalui path relatif. Path impor aktual adalah alamat relatif dari alamat absolut file relatif terhadap pb_learn. Lihat contoh berikut player.proto mengimpor file lain:
import "common.proto";
import "monster/monster.proto";
import "player/health.proto";Bahkan health.proto dalam direktori yang sama sekarang harus menggunakan path relatif. Oleh karena itu, dalam proyek, kami biasanya membuat folder terpisah untuk menyimpan semua file protobuf dan menetapkannya sebagai path pemindaian selama kompilasi. Semua perilaku impor di direktori itu juga berdasarkan path relatifnya.
TIP
Jika Anda menggunakan editor GoLand, untuk direktori protobuf yang Anda buat, secara default tidak dapat diuraikan, menghasilkan highlight merah. Untuk membuat GoLand mengenalinya, Anda perlu mengatur path pemindaian secara manual. Prinsipnya persis sama seperti yang dijelaskan di atas. Metode pengaturannya adalah sebagai berikut: buka pengaturan berikut:
File | Settings | Languages & Frameworks | Protocol BuffersTambahkan path pemindaian secara manual di Import Paths, yang harus konsisten dengan path yang ditentukan selama kompilasi.

Any
Tipe Any memungkinkan Anda menggunakan pesan sebagai tipe tertanam tanpa memerlukan definisi proto mereka. Anda dapat langsung mengimpor tipe yang didefinisikan Google, yang sudah built-in dan tidak perlu ditulis secara manual.
import "google/protobuf/any.proto";
message ErrorStatus {
string message = 1;
repeated google.protobuf.Any details = 2;
}Google juga telah mendefinisikan banyak tipe lainnya. Kunjungi protobuf/ptypes at master · golang/protobuf (github.com) untuk melihat lebih banyak, terutama termasuk:
- Wrapper tipe dasar
- Tipe waktu
- Tipe Duration
Definisi protobuf mereka seharusnya ada di direktori include compiler protoc.
OneOf
Penjelasan dokumentasi resmi di sini terlalu bertele-tele. Sederhananya, ini berarti field dapat memiliki beberapa tipe yang mungkin selama transmisi, tetapi pada akhirnya hanya satu tipe yang akan digunakan. Interior tidak dapat berisi field yang dimodifikasi dengan repeated, seperti union di C.
message Stock {
// Data khusus Stock
}
message Currency {
// Data khusus Currency
}
message ChangeNotification {
int32 id = 1;
oneof instrument {
Stock stock = 2;
Currency currency = 3;
}
}Service
Kata kunci service dapat mendefinisikan layanan RPC. Layanan RPC berisi beberapa antarmuka RPC, yang dibagi menjadi antarmuka unary dan antarmuka streaming.
message Body {
string name = 1;
}
service ExampleService {
rpc DoSomething(Body) returns(Body);
}Antarmuka streaming dibagi lagi menjadi streaming searah dan streaming dua arah, biasanya dimodifikasi dengan kata kunci stream. Lihat contoh berikut:
message Body {
string name = 1;
}
service ExampleService {
// Streaming klien
rpc DoSomething(stream Body) returns(Body);
// Streaming server
rpc DoSomething1(Body) returns(stream Body);
// Streaming dua arah
rpc DoSomething2(stream Body) returns(stream Body);
}Streaming berarti pengiriman data jangka panjang dalam koneksi, tidak lagi sesederhana tanya-jawab seperti antarmuka unary.
Empty
Empty sebenarnya adalah message kosong, sesuai dengan struct kosong di Go. Ini jarang digunakan untuk memodifikasi field dan terutama digunakan untuk menunjukkan bahwa antarmuka RPC tidak memerlukan parameter atau tidak memiliki nilai kembali.
syntax = "proto3";
import "google/protobuf/empty.proto";
service EmptyService {
rpc Do(google.protobuf.Empty) returns(google.protobuf.Empty);
}Option
Option biasanya digunakan untuk mengontrol beberapa perilaku protobuf. Misalnya, untuk mengontrol package yang dihasilkan untuk kode sumber bahasa Go, Anda dapat mendeklarasikan sebagai berikut:
option go_package = "github/jack/sample/pb_learn;pb_learn"Sebelum titik koma adalah path impor untuk file sumber lain setelah generasi kode, dan setelah titik koma adalah nama package untuk file yang dihasilkan yang sesuai.
Ini dapat melakukan beberapa optimasi dengan nilai yang tersedia berikut, yang tidak dapat dideklarasikan berulang kali:
SPEED: Tingkat optimasi tertinggi, volume kode yang dihasilkan terbesar, ini adalah default.CODE_SIZE: Mengurangi volume kode yang dihasilkan tetapi bergantung pada refleksi untuk serialisasi.LITE_RUNTIME: Volume kode terkecil tetapi tidak memiliki beberapa fitur.
Berikut adalah contoh penggunaan:
option optimize_for = SPEED;Selain itu, option juga dapat menambahkan beberapa metadata ke message dan enum. Anda bisa mendapatkan informasi ini melalui refleksi, yang sangat berguna untuk validasi parameter.
Kompilasi
Kompilasi adalah generasi kode. Di atas kami hanya mendefinisikan file protobuf. Dalam penggunaan aktual, mereka perlu dikonversi menjadi kode sumber bahasa spesifik untuk digunakan. Kami menyelesaikan ini melalui compiler protoc, yang mendukung beberapa bahasa.

Instalasi
Untuk mengunduh compiler, pergi ke protocolbuffers/protobuf: Protocol Buffers - Google's data interchange format (github.com) untuk mengunduh Release terbaru, umumnya file kompresi:
protoc-25.1-win64
│ readme.txt
│
├─bin
│ protoc.exe
│
└─include
└─google
└─protobuf
│ any.proto
│ api.proto
│ descriptor.proto
│ duration.proto
│ empty.proto
│ field_mask.proto
│ source_context.proto
│ struct.proto
│ timestamp.proto
│ type.proto
│ wrappers.proto
│
└─compiler
plugin.protoSetelah mengunduh, tambahkan direktori bin ke environment variables untuk menggunakan perintah protoc. Setelah selesai, periksa versi. Output normal menunjukkan instalasi berhasil:
$ protoc --version
libprotoc 25.1Compiler yang diunduh tidak mendukung bahasa Go secara default karena generasi kode bahasa Go adalah executable terpisah, sementara bahasa lain digabungkan bersama. Jadi instal plugin bahasa Go untuk menerjemahkan definisi protobuf menjadi kode sumber bahasa Go:
$ go install google.golang.org/protobuf/cmd/protoc-gen-go@latestJika Anda juga perlu menghasilkan kode layanan gRPC, instal plugin berikut:
$ go install google.golang.org/grpc/cmd/protoc-gen-go-grpc@latestSetelah instalasi, periksa versi mereka:
$ protoc-gen-go-grpc --version
protoc-gen-go-grpc 1.3.0
$ protoc-gen-go --version
protoc-gen-go.exe v1.31.0Plugin ini juga merupakan file biner terpisah, tetapi hanya dapat dipanggil melalui protoc dan tidak dapat dieksekusi secara independen:
(this program should be run by protoc, not directly)Selain itu, ada banyak plugin lain, seperti plugin untuk menghasilkan dokumentasi antarmuka openapi, dll. Jika tertarik, Anda dapat mencari sendiri.
Generasi
Masih menggunakan contoh sebelumnya, strukturnya adalah sebagai berikut:
pb_learn
│ common.proto
│
├─monster
│ monster.proto
│
└─player
health.proto
player.protoUntuk generasi kode, tiga parameter perlu ditentukan total:
- Path pemindaian: memberi tahu compiler di mana mencari file
protobufdan bagaimana menguraikan path impor. - Path generasi: tempat file yang dikompilasi ditempatkan.
- File target: menentukan file target mana yang perlu dikompilasi.
Sebelum memulai, pastikan go_package dalam file protobuf diatur dengan benar. Gunakan protoc -h untuk memeriksa parameter yang didukung. Yang paling umum digunakan adalah -I atau --proto_path, yang dapat digunakan beberapa kali untuk menentukan beberapa path pemindaian, misalnya:
$ protoc --proto_path=./pb_learn --proto_path=./third_partyHanya menentukan path pemindaian tidak cukup; Anda juga perlu menentukan path generasi dan file protobuf target. Di sini kami menghasilkan file Go, jadi gunakan parameter --go_out, didukung oleh plugin protoc-gen-go yang diunduh sebelumnya:
$ cd pb_learn
$ protoc --proto_path=. --go_out=. common.proto
$ ls
common.pb.go common.proto monster/ player/Parameter --go_out menentukan path generasi. . berarti path saat ini, dan common.proto menentukan file yang akan dikompilasi. Jika Anda ingin menghasilkan kode grpc (prasyarat: plugin grpc terinstal), Anda dapat menambahkan parameter --go-grpc_out (jika file protobuf tidak mendefinisikan service, file yang sesuai tidak akan dihasilkan):
$ protoc --proto_path=. --go_out=. --go-grpc_out=. common.proto
$ ls
common.pb.go common.proto common_grpc.pb.go monster/ player/common.pb.go adalah definisi tipe protobuf yang dihasilkan, dan common_grpc.pb.go adalah kode gRPC yang dihasilkan, yang didasarkan pada yang pertama. Jika definisi bahasa tidak dihasilkan, kode gRPC tidak dapat dihasilkan.
Jika Anda ingin mengompilasi semua file protobuf di direktori, Anda dapat menggunakan wildcard *, seperti *.proto:
$ protoc --proto_path=. --go_out=. --go-grpc_out=. ./*.protoJika Anda ingin menyertakan semua file, Anda dapat menggunakan wildcard **, seperti ./**/*.proto:
$ protoc --proto_path=. --go_out=. --go-grpc_out=. ./**/*.protoNamun, metode ini hanya berlaku untuk shell yang mendukung wildcard ini. Misalnya, di Windows, cmd atau powershell tidak mendukung penulisan ini:
D> protoc --proto_path=. --go_out=. --go-grpc_out=. ./**/*.proto
Invalid file name pattern or missing input file "./**/*.proto"Untungnya, gitbash mendukung banyak perintah Linux dan juga dapat membuat Windows mendukung sintaks ini. Untuk menghindari menulis perintah berulang setiap kali, Anda dapat menempatkannya di makefile:
.PHONY: all
proto_gen:
protoc --proto_path=. \
--go_out=paths=source_relative:. \
--go-grpc_out=paths=source_relative:. \
./**/*.proto ./*.protoAnda dapat melihat ada tambahan paths=source_relative:, yang mengatur mode path generasi file. Ada opsi berikut:
paths=import: Ini adalah default. File dihasilkan di direktori yang ditentukan olehimport. Ini juga dapat menjadi path modul. Misalnya, jika ada fileprotos/buzz.protodan Anda menentukanpaths=example.com/project/protos/fizz, akhirnya akan menghasilkanexample.com/project/protos/fizz/buzz.pb.go.module=$PREFIX: Selama generasi, prefix path akan dihapus. Dalam contoh di atas, jika Anda menentukan prefixexample.com/project, akhirnya akan menghasilkanprotos/fizz/buzz.pb.go. Mode ini terutama digunakan untuk menghasilkan langsung di modul (terasa seperti tidak ada perbedaan).paths=source_relative: File yang dihasilkan mempertahankan struktur relatif yang sama dengan fileprotobufdi direktori yang ditentukan.
Setelah titik dua : adalah path generasi yang ditentukan:
| common.proto
| common.pb.go
│
├─monster
│ monster.pb.go
│ monster.proto
│
└─player
health.pb.go
health.proto
health_grpc.pb.go
player.pb.go
player.protoRefleksi
Anda dapat memperluas enum dan message melalui options. Pertama, impor "google/protobuf/descriptor.proto":
import "google/protobuf/descriptor.proto";
extend google.protobuf.EnumValueOptions {
optional string string_name = 123456789;
}
enum Integer {
INT64 = 0[
(string_name) = "int_64"
];
}Ini setara dengan menambahkan metadata ke nilai enum. Hal yang sama berlaku untuk message:
import "google/protobuf/descriptor.proto";
extend google.protobuf.MessageOptions {
optional string my_option = 51234;
}
message MyMessage {
option (my_option) = "Hello world!";
}Ini setara dengan memiliki refleksi tentang protobuf. Setelah menghasilkan kode, Anda dapat mengaksesnya melalui Descriptor:
func main() {
message := pb_learn.MyMessage{}
message.ProtoReflect().Descriptor().Options().ProtoReflect().Range(func(descriptor protoreflect.FieldDescriptor, value protoreflect.Value) bool {
fmt.Println(descriptor.FullName(), ":", value)
return true
})
}Output:
my_option:"Hello world!"Pendekatan ini dapat dibandingkan dengan menambahkan tag ke struct di Go; rasanya mirip. Berdasarkan pendekatan ini, Anda juga dapat mengimplementasikan fungsionalitas validasi parameter. Anda hanya perlu menulis aturan di options dan memeriksa melalui Descriptor.
