Skip to content

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.

protobuf
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"; proto3 sözdiziminin kullanıldığını gösterir, varsayılan olarak proto3 sözdizimi kullanılır.
  • message struct'a benzer şekilde bildirilir, proto'nun temel yapısıdır
  • SearchRequest içinde üç alan tanımlanmıştır, her alanın adı ve türü vardır
  • service iç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 message olmalı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.

protobuf
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 TypeGo Type
doublefloat64
floatfloat32
int32int32
int64int64
uint32uint32
uint64uint64
sint32int32
sint64int64
fixed32uint32
fixed64uint64
sfixed32int32
sfixed64int64
boolbool
stringstring
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.

protobuf
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

protobuf
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ış bir message'da bu alandan 0 veya 1 tane vardır, yani aynı alan tekrarlanamaz. Aşağıdaki bildirim hata verir.

    protobuf
    syntax = "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 olabilir

    • set: Serileştirilecektir
    • unset: 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 gibidir

    protobuf
    map<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.

protobuf
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.

protobuf
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.

protobuf
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

protobuf
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

protobuf
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.

protobuf
package foo.bar;
message Open { ... }

Ardından mesaj türünü tanımlayan alanda paket adını kullanabilirsiniz:

protobuf
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.

protobuf
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.proto

Eğ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.

protobuf
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.

go
import "../common.proto"; // Yanlış yazım

TIP

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.

protobuf
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 Buffers

Import 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.

protobuf
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.

protobuf
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.

protobuf
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.

protobuf
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.

protobuf
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.

protobuf
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ır
  • LIFE_RUNTIME, kod hacmi en küçük, ancak bazı özellikler eksik olur.

Aşağıda bir kullanım örneği vardır

protobuf
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

bash
$ 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.

bash
$ go install google.golang.org/protobuf/cmd/protoc-gen-go@latest

Eğer gRPC servis kodu da oluşturmanız gerekiyorsa aşağıdaki eklentiyi yükleyin

bash
$ go install google.golang.org/grpc/cmd/protoc-gen-go-grpc@latest

Kurulumdan sonra versiyonunu kontrol edin

bash
$ protoc-gen-go-grpc --version
protoc-gen-go-grpc 1.3.0

$ protoc-gen-go --version
protoc-gen-go.exe v1.31.0

Bu 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.proto

Kod oluşturma için toplamda üç parametre belirtilmelidir

  1. Tarama yolu, derleyiciye protobuf dosyalarını nerede arayacağını ve içe aktarma yolunu nasıl ayrıştıracağını gösterir
  2. Oluşturma yolu, derlendikten sonra dosyanın nereye konulacağını
  3. 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

bash
$ protoc --proto_path=./pb_learn --proto_path=./third_party

Sadece 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.

bash
$ 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).

bash
$ 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

bash
$ protoc --proto_path=. --go_out=.. common.proto --go-grpc_out=. ./*.proto

Eğer tüm dosyaları dahil etmek istiyorsanız ** joker karakterini kullanabilirsiniz, örneğin ./**/*.proto.

bash
$ protoc --proto_path=. --go_out=.. common.proto --go-grpc_out=. ./**/*.proto

Ancak 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

powershell
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

makefile
.PHONY: all

proto_gen:
  protoc --proto_path=. \
       --go_out=paths=source_relative:. \
       --go-grpc_out=paths=source_relative:. \
       ./**/*.proto ./*.proto

paths=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, dosya import tarafından belirtilen dizinde oluşturulur, bir modül yolu da olabilir. Örneğin protos/buzz.proto dosyası varsa, paths=example.com/project/protos/fizz belirtilirse, sonunda example.com/project/protos/fizz/buzz.pb.go oluşturulur.
  • module=$PREFIX, oluştururken yol öneki silinir. Yukarıdaki örnekte önek example.com/project belirtilirse, sonunda protos/fizz/buzz.pb.go oluş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 dizinde protobuf dosyası 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.proto

Yansıma

options aracılığıyla enum ve message genişletilebilir, önce "google/protobuf/descriptor.proto" içe aktarın

protobuf
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

protobuf
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

go
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.

Golang by www.golangdev.cn edit