Protobuf
Resmi web sitesi: Protocol Buffers | Google Developers
Tanıtım
Resmi öğretici: Protocol Buffer Basics: Go | Protocol Buffers | Google Developers
Protocol Buffers, Google'ın 2008 yılında açık kaynak yaptığı dilden bağımsız, protokolden bağımsız, genişletilebilir yapılandırılmış veri serileştirme mekanizmasıdır. Paket açma ve paketleme sırasında daha hızlıdır, çoğunlukla RPC alanı iletişimi ile ilgilidir, verilerin yapılandırılmış yolunu tanımlayabilir ve ardından yapılandırılmış verileri çeşitli veri akışlarına yazmak ve çeşitli veri akışlarından okumak için özel oluşturulmuş kaynak kodunu kolayca kullanabilir ve çeşitli dillerde kullanılabilir, Protocol Buffers aşağıda protobuf olarak anılacaktır.
protobuf oldukça popülerdir, özellikle go alanında, gRPC bunu protokol iletimi için serileştirme mekanizması olarak kullanır.
Sözdizimi
Öncelikle bir örnekten protobuf dosyasının genel olarak nasıl göründüğüne bakalım, genel olarak sözdizimi çok basittir, on beş dakikada başlayabilirsiniz. Aşağıda search.proto adlı bir dosya örneği vardır, protobuf dosya uzantısı .proto'dur.
syntax = "proto3";
message SearchRequest {
string query = 1;
string number = 2;
}
message SearchResult {
string data = 1;
}
service SearchService {
rpc Search(SearchRequest) returns(SearchResult);
}- İlk satır
syntax = "proto3";proto3sözdiziminin kullanıldığını gösterir, varsayılan olarakproto3sözdizimi kullanılır. messagestruct'a benzer şekilde bildirilir,proto'nun temel yapısıdırSearchRequestiçinde üç alan tanımlanmıştır, her alanın adı ve türü vardırserviceiçinde bir servis tanımlanmıştır, bir servis bir veya birden fazla rpc arayüzü içerir- rpc arayüzünün mutlaka ve sadece bir parametresi ve dönüş değeri olmalıdır, türleri mutlaka
messageolmalıdır, temel tür olamaz.
Ayrıca dikkat edilmesi gereken, proto dosyasındaki her satırın sonunda mutlaka noktalı virgül olmalıdır.
Yorumlar
Yorum stili go ile tamamen aynıdır.
syntax = "proto3";
/* Yorum
* Yorum */
message SearchRequest {
string query = 1; //Yorum
string number = 2;
}Türler
Tür değiştiriciler sadece message içinde görünebilir, tek başına görülemez.
Temel Türler
| 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 |
Dizi
Temel türün önüne repeated değiştiricisi eklemek bunun bir dizi türü olduğunu gösterir, go'daki slice'a karşılık gelir.
message Company {
repeated string employee = 1;
}map
protobuf'te map türü tanımlama formatı şu şekildedir
map<key_type, value_type> map_field = N;key_type sayı veya string olmalıdır, value_type tür kısıtlaması yoktur, bir örneğe bakalım
message Person {
map<string, int64> cards = 1;
}Alanlar
Aslında proto geleneksel anahtar-değer türü değildir, bildirilen proto dosyasında belirli veriler görünmez, her alanın = işaretinden sonra gelen mevcut message'daki benzersiz numara olmalıdır, bu numaralar binary mesaj gövdesinde bu alanları tanımlamak ve ayırt etmek için kullanılır. Numaralar 1'den başlar, 1-15 numaralar 1 bayt yer kaplar, 16-2047 iki bayt yer kaplar, bu nedenle sık görünen alanlara mümkün olduğunca 1-15 numarası vererek yer tasarrufu sağlayın ve daha sonra sık görünebilecek alanlar için biraz yer bırakın.
Bir message'daki alanlar aşağıdaki kurallara uymalıdır
singular: Varsayılan olarak bu türün alanıdır, iyi yapılandırılmış birmessage'da bu alandan 0 veya 1 tane vardır, yani aynı alan tekrarlanamaz. Aşağıdaki bildirim hata verir.protobufsyntax = "proto3"; message SearchRequest { string query = 1; string number = 2; string number = 3;//Alan tekrarlanır }optional:singular'a benzer, sadece alan değerinin ayarlanıp ayarlanmadığını açıkça kontrol edebilir, aşağıdaki iki durum olabilirset: Serileştirilecektirunset: Serileştirilmeyecektir
repeated: Bu türdeki alan 0 veya birden fazla kez görünebilir, tekrarlayan değerler sıraya göre saklanacaktır (basitçe söylemek gerekirse dizi, aynı türdeki değerin birden fazla kez tekrarlanmasına izin verir ve göründüğü sıraya göre saklar, indekstir)map: Anahtar-değer çifti türündeki alan, bildirim yöntemi aşağıdaki gibidirprotobufmap<string,int32> config = 3;
Ayrılmış Alanlar
reserve anahtar kelimesi ayrılmış alanları bildirebilir, ayrılmış alan numarası bildirildikten sonra başka alan numarası ve adı olarak kullanılamaz, derleme sırasında da hata oluşur. Google'ın resmi cevabı: Eğer bir proto dosyası yeni bir sürümde bazı numaraları silerse, gelecekte diğer kullanıcılar bu silinmiş numaraları yeniden kullanabilir, ancak eski sürüm numaralarına geri dönülürse alanlara karşılık gelen numaraların tutarsız olması hataya neden olur, ayrılmış alan derleme döneminde bu uyarı işlevini görebilir, bu ayrılmış alanı kullanamayacağınızı hatırlatır, aksi takdirde derleme geçmez.
syntax = "proto3";
message SearchRequest {
string query = 1;
string number = 2;
map<string, int32> config = 3;
repeated string a = 4;
reserved "a"; //Belirli adlı alanı ayrılmış alan olarak bildir
reserved 1 to 2; //Bir numara dizisini ayrılmış alan olarak bildir
reserved 3,4; //Bildır
}Bu şekilde bu dosya derlenmez.
Kullanımdan Kaldırılmış Alanlar
Eğer bir alan kullanımdan kaldırıldıysa, aşağıdaki gibi yazılabilir.
message Body {
string name = 1 [deprecated = true];
}Enum
Enum sabitleri bildirilebilir ve alan türü olarak kullanılabilir, dikkat edilmesi gereken, enum öğesinin ilk elemanı sıfır olmalıdır, çünkü enum öğesinin varsayılan değeri ilk elemandır.
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;
}Enum öğesi içinde aynı değere sahip enum öğeleri olduğunda enum takma adı kullanılabilir
syntax = "proto3";
enum Type {
option allow_alias = true; //Takma adı kullanmaya izin veren yapılandırma öğesini açmak gerekir
GET = 0;
GET_ALIAS = 0; //GET enum öğesinin takma adı
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;
}İç İçe Mesajlar
message Outer { // Seviye 0
message MiddleAA { // Seviye 1
message Inner { // Seviye 2
int64 ival = 1;
bool booly = 2;
}
}
message MiddleBB { // Seviye 1
message Inner { // Seviye 2
int32 ival = 1;
bool booly = 2;
}
}
}message içinde message bildirilebilir, iç içe struct gibi.
Package
Protokol mesaj türleri arasındaki isim çakışmalarını önlemek için protobuf dosyasına isteğe bağlı bir paket değiştiricisi ekleyebilirsiniz.
package foo.bar;
message Open { ... }Ardından mesaj türünü tanımlayan alanda paket adını kullanabilirsiniz:
message Foo {
...
foo.bar.Open open = 1;
...
}Import
İçe aktarma birden fazla protobuf dosyasının tanım paylaşmasına olanak tanır, sözdizimi şu şekildedir, içe aktarırken dosya uzantısı atlanamaz.
import "a/b/c.proto";İçe aktarma sırasında göreli yol kullanılır, bu göreli yol içe aktaran dosya ile içe aktarılan dosyanın göreli yolu değil, protoc derleyicisinin kod oluştururken belirttiği tarama yoluna bağlıdır, aşağıdaki dosya yapısı olduğunu varsayalım
pb_learn
│ common.proto
│
├─monster
│ monster.proto
│
└─player
health.proto
player.protoEğer sadece player dizini kısmının kodunu oluşturmamız gerekiyorsa ve tarama yolunu sadece player dizini olarak belirttiysek, health.proto ile player.proto arasındaki karşılıklı içe aktarma doğrudan tek dosya adı yazabilir, örneğin player.proto health.proto'yu içe aktarır.
import "health.proto";Eğer bu sırada player.proto common.proto veya monster dizinindeki dosyaları içe aktarıyorsa derleme başarısız olur, bu nedenle aşağıdaki yazım tamamen yanlıştır, çünkü derleyici bu dosyaları bulamaz.
import "../common.proto"; // Yanlış yazımTIP
Ayrıca belirtmek gerekir ki, .., . bu sembollerin içe aktarma yolunda görünmesine izin verilmez.
Derleme sırasında pb_learn tarama yolu olarak belirtilirse diğer dizinlerin dosyalarını göreli yol ile içe aktarabilirsiniz, gerçek içe aktarma yolu bu dosyanın mutlak adresinin pb_learn'e göreli adresidir, aşağıda player.proto'nun diğer dosyaları içe aktarma örneğine bakalım.
import "common.proto";
imrpot "monster/monster.proto";
import "player/health.proto";Aynı dizindeki health.proto bile şimdi göreli yol kullanmak zorundadır. Bu nedenle bir projede genellikle tüm protobuf dosyalarını koymak için ayrı bir klasör oluştururuz ve derleme sırasında bunu tarama yolu olarak belirtiriz, bu dizindeki tüm içe aktarma davranışları da buna göreli yoldur.
TIP
Eğer goland editörü kullanıyorsanız, kendi oluşturduğunuz protobuf dizini varsayılan olarak ayrıştırılamaz, yani kırmızı patlama durumu olur, goland'ın tanımasını istiyorsanız tarama yolunu manuel olarak ayarlamanız gerekir, prensibi yukarıda anlatılanla tamamen aynıdır, ayarlama yöntemi aşağıdaki gibi, aşağıdaki ayarları açın
File | Settings | Languages & Frameworks | Protocol BuffersImport Paths'de tarama yolunu manuel olarak ekleyin, bu tarama yolu derleme sırasında belirttiğiniz yolla tutarlı olmalıdır.

Any
Any türü mesajları gömülü tür olarak kullanmanıza olanak tanır, proto tanımlarına ihtiyaç duymadan, doğrudan Google'ın tanımladığı türleri içe aktarabiliriz, kendi içinde gelir, manuel yazmaya gerek yoktur.
import "google/protobuf/any.proto";
message ErrorStatus {
string message = 1;
repeated google.protobuf.Any details = 2;
}Google ayrıca başka birçok tür önceden tanımlamıştır, daha fazlası için protobuf/ptypes at master · golang/protobuf (github.com) adresini ziyaret edin, başlıca şunlar dahil
- Temel türlerin sarmalanması
- Zaman türü
- Duration türü
Bunların protobuf tanımları protoc derleyicisinin include dizininde olmalıdır.
OneOf
Burada resmi dokümantasyonun verdiği açıklama çok karmaşık, insan diliyle söylemek gerekirse bir alanın iletim sırasında birden fazla olası türe sahip olabileceğini, ancak sonunda sadece bir türün kullanılacağını gösterir, içinde repeated değiştiricisi ile değiştirilmiş alanlar görünemez, bu tıpkı c dilindeki union gibidir.
message Stock {
// Stoka özgü veri
}
message Currency {
// Para birimine özgü veri
}
message ChangeNotification {
int32 id = 1;
oneof instrument {
Stock stock = 2;
Currency currency = 3;
}
}Service
service anahtar kelimesi bir RPC servisi tanımlayabilir, bir RPC servisi birkaç rpc arayüzü içerir, arayüzler unary arayüz ve stream arayüz olarak ayrılır.
message Body {
string name = 1;
}
service ExampleService {
rpc DoSomething(Body) returns(Body);
}Stream arayüzü tek yönlü stream ve çift yönlü stream olarak ayrılır, genellikle stream anahtar kelimesi ile değiştirilir, aşağıdaki bir örneğe bakalım.
message Body {
string name = 1;
}
service ExampleService {
// İstemci stream
rpc DoSomething(stream Body) returns(Body);
// Sunucu stream
rpc DoSomething1(Body) returns(stream Body);
// Çift yönlü stream
rpc DoSomething2(stream Body) returns(stream Body);
}Stream denilen bir bağlantıda uzun süreli karşılıklı veri göndermektir, unary arayüz gibi basit soru-cevap değildir.
Empty
empty aslında boş bir message'dır, go'daki boş struct'a karşılık gelir, nadiren alanları değiştirmek için kullanılır, esas olarak belirli rpc arayüzünün parametre gerektirmediğini veya dönüş değeri olmadığını göstermek için kullanılır.
syntax = "proto3";
import "google/protobuf/empty.proto";
service EmptyService {
rpc Do(google.protobuf.Empty) returns(google.protobuf.Empty);
}Option
option genellikle protobuf'un bazı davranışlarını kontrol etmek için kullanılır. Örneğin go dil kaynak kodu oluşturulan paketi kontrol etmek, aşağıdaki gibi bildirilebilir.
option go_package = "github/jack/sample/pb_learn;pb_learn"Noktalı virgülün önü kod oluşturulduktan sonra diğer kaynak dosyaların içe aktarma yoludur, noktalı virgülün arkası ilgili oluşturulan dosyanın paket adıdır.
Bazı optimizasyonlar yapabilir, aşağıdaki birkaç kullanılabilir değer vardır, tekrar bildirilemez.
SPEED, en yüksek optimizasyon derecesi, oluşturulan kod hacmi en büyük, varsayılan bu.CODE_SIZE, kod oluşturma hacmini azaltır, ancak serileştirme için yansıma kullanırLIFE_RUNTIME, kod hacmi en küçük, ancak bazı özellikler eksik olur.
Aşağıda bir kullanım örneği vardır
option optimize_for = SPEED;Bunun dışında option message ve enum'a bazı meta bilgileri ekleyebilir, yansıma kullanılarak bu bilgiler alınabilir, bu parametre doğrulaması yaparken özellikle kullanışlıdır.
Derleme
Derleme yani kod oluşturma, yukarıda sadece protobuf dosyası tanımlandı, gerçek kullanımda belirli bir dil kaynak koduna dönüştürülmesi gerekir, bunu protoc derleyicisi ile tamamlıyoruz, birden fazla dili destekler.

Kurulum
Derleyici indirmek için protocolbuffers/protobuf: Protocol Buffers - Google's data interchange format (github.com) adresinden en son Release'i indirin, genellikle bir sıkıştırılmış dosyadır
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.protoİndirdikten sonra bin dizinini ortam değişkenlerine ekleyin, protoc komutunu kullanabilmek için, tamamlandıktan sonra versiyonu kontrol edin, normal çıktı alırsanız kurulum başarılı demektir
$ protoc --version
libprotoc 25.1İndirilen derleyici varsayılan olarak go dilini desteklemez, çünkü go dil kod oluşturma ayrı bir çalıştırılabilir dosyadır, diğer dillerin hepsi bir arada, bu nedenle go dil eklentisini yükleyin, protocbuf tanımını go dil kaynak koduna çevirmek için kullanılır.
$ go install google.golang.org/protobuf/cmd/protoc-gen-go@latestEğer gRPC servis kodu da oluşturmanız gerekiyorsa aşağıdaki eklentiyi yükleyin
$ go install google.golang.org/grpc/cmd/protoc-gen-go-grpc@latestKurulumdan sonra versiyonunu kontrol edin
$ protoc-gen-go-grpc --version
protoc-gen-go-grpc 1.3.0
$ protoc-gen-go --version
protoc-gen-go.exe v1.31.0Bu eklentiler de ayrı binary dosyalardır, ancak sadece protoc aracılığıyla çağrılabilir, tek başına çalıştırılamaz.
(this program should be run by protoc, not directly)Bunun dışında birçok başka eklenti vardır, örneğin openapi arayüz dokümanı oluşturan eklenti vb., ilgileniyorsanız kendiniz arayabilirsiniz.
Oluşturma
Hala önceki örneği alarak anlatıyorum, yapı şu şekildedir
pb_learn
│ common.proto
│
├─monster
│ monster.proto
│
└─player
health.proto
player.protoKod oluşturma için toplamda üç parametre belirtilmelidir
- Tarama yolu, derleyiciye
protobufdosyalarını nerede arayacağını ve içe aktarma yolunu nasıl ayrıştıracağını gösterir - Oluşturma yolu, derlendikten sonra dosyanın nereye konulacağını
- Hedef dosya, hangi hedef dosyaların derleneceğini belirtir.
Başlamadan önce protobuf dosyasındaki go_package ayarının doğru olduğundan emin olun, protoc -h aracılığıyla desteklediği parametreleri kontrol edin, en yaygın -I veya --proto_path, birden fazla tarama yolu belirtmek için birden fazla kez kullanılabilir, örneğin
$ protoc --proto_path=./pb_learn --proto_path=./third_partySadece tarama yolu belirtmek yeterli değildir, oluşturma yolu ve hedef protobuf dosyası da belirtilmelidir, burada go dosyası oluşturulduğu için --go_out parametresi kullanılır, daha önce indirilen protoc-gen-go eklentisi tarafından desteklenir.
$ cd pb_learn
$ protoc --proto_path=. --go_out=. common.proto
$ ls
common.pb.go common.proto monster/ player/--go_out parametresi oluşturma yolunu belirtir, . mevcut yolu gösterir, common.proto derlenecek dosyayı belirtir. Eğer grpc kodu oluşturmak istiyorsanız (ön koşul grpc eklentisi kurulu), --go-grpc_out parametresi eklenebilir (eğer protobuf dosyasında service tanımlanmamışsa karşılık gelen dosya oluşturulmaz).
$ 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 oluşturulan protobuf tür tanımıdır, common_grpc.pb.go oluşturulan gRPC kodudur,前者'ine dayanır, eğer karşılık gelen dilin tanımı oluşturulmamışsa gRPC kodu da oluşturulamaz.
Eğer bu dizindeki tüm protobuf dosyalarını derlemek istiyorsanız * joker karakterini kullanabilirsiniz, örneğin
$ protoc --proto_path=. --go_out=.. common.proto --go-grpc_out=. ./*.protoEğer tüm dosyaları dahil etmek istiyorsanız ** joker karakterini kullanabilirsiniz, örneğin ./**/*.proto.
$ protoc --proto_path=. --go_out=.. common.proto --go-grpc_out=. ./**/*.protoAncak bu yöntem sadece bu joker karakteri destekleyen shell için geçerlidir, örneğin windows altında cmd veya powershell bu yazımı desteklemez
D> protoc --proto_path=. --go_out=.. common.proto --go-grpc_out=. ./**/*.proto
Invalid file name pattern or missing input file "./**/*.proto"Neyse ki gitbash linux birçok komutu destekler, windows'un da bu sözdizimini desteklemesini sağlayabilir. Her seferinde tekrarlayan komutları yazmaktan kaçınmak için bunları makefile içine koyabilirsiniz
.PHONY: all
proto_gen:
protoc --proto_path=. \
--go_out=paths=source_relative:. \
--go-grpc_out=paths=source_relative:. \
./**/*.proto ./*.protopaths=source_relative:. eklendiğine dikkat edin, bu dosya oluşturma yol modunu ayarlıyor, toplamda aşağıdaki birkaç seçenek var
paths=import, varsayılan bu, dosyaimporttarafından belirtilen dizinde oluşturulur, bir modül yolu da olabilir. Örneğinprotos/buzz.protodosyası varsa,paths=example.com/project/protos/fizzbelirtilirse, sonundaexample.com/project/protos/fizz/buzz.pb.gooluşturulur.module=$PREFIX, oluştururken yol öneki silinir. Yukarıdaki örnekte önekexample.com/projectbelirtilirse, sonundaprotos/fizz/buzz.pb.gooluşturulur, bu modül doğrudan modül içinde oluşturmak için kullanılır (farkı yok gibi hissediyorum).paths=source_relative, oluşturulan dosya belirtilen dizindeprotobufdosyası ile aynı göreli yapıyı korur.
İki nokta : ile ayrıldıktan sonra belirtilen oluşturma yoludur.
| 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.protoYansıma
options aracılığıyla enum ve message genişletilebilir, önce "google/protobuf/descriptor.proto" içe aktarın
import "google/protobuf/descriptor.proto";
extend google.protobuf.EnumValueOptions {
optional string string_name = 123456789;
}
enum Integer {
INT64 = 0[
(string_name) = "int_64"
];
}Bu bu enum değerine bir meta bilgi eklemeye eşdeğerdir. message için de aynı şekilde, aşağıdaki gibi
import "google/protobuf/descriptor.proto";
extend google.protobuf.MessageOptions {
optional string my_option = 51234;
}
message MyMessage {
option (my_option) = "Hello world!";
}Bu protobuf ile ilgili yansıma gibidir, kod oluşturulduktan sonra Descriptor aracılığıyla erişilebilir, aşağıdaki gibi
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
})
}Çıktı
my_option:"Hello world!"Bu yol go'da struct'a tag eklemeye benzetilebilir, hepsi aynı his, bu yolla parametre doğrulama işlevi de实现 edilebilir, sadece options'ta kuralları yazın, Descriptor aracılığıyla kontrol edin.
