gRPC-Protocol基礎知識-C#篇

ddockerman發表於2020-09-25

本文使用協議緩衝區語言的proto3版本,為C#程式設計師提供了使用協議緩衝區的基本介紹。 通過建立一個簡單的示例應用程式,展示瞭如何

為什麼要使用協議緩衝區?

我們將使用的示例是一個非常簡單的“地址簿”應用程式,該應用程式可以在檔案中讀寫人的聯絡方式。通訊錄中的每個人都有一個姓名,一個ID,一個電子郵件地址和一個聯絡電話。

您如何像這樣序列化和檢索結構化資料?有幾種方法可以解決此問題:

將.NET二進位制序列化與System.Runtime.Serialization.Formatters.Binary.BinaryFormatter和關聯的類一起使用。面對變化,這最終變得非常脆弱,在某些情況下,資料大小非常昂貴。如果您需要與為其他平臺編寫的應用程式共享資料,它也不是很好。
您可以發明一種將資料項編碼為單個字串的臨時方法,例如將4個整數編碼為“ 12:3:-23:67”。儘管確實需要編寫一次性的編碼和解析程式碼,但是這是一種簡單且靈活的方法,而且解析帶來的執行時成本很小。這對於編碼非常簡單的資料最有效。
將資料序列化為XML。由於XML是人類(一種)可讀的,並且存在用於多種語言的繫結庫,因此這種方法可能非常有吸引力。如果要與其他應用程式/專案共享資料,這可能是一個不錯的選擇。但是,眾所周知,XML佔用大量空間,對它進行編碼/解碼會給應用程式帶來巨大的效能損失。同樣,導航XML DOM樹比通常導航類中的簡單欄位要複雜得多。
協議緩衝區是靈活,高效,自動化的解決方案,可以準確地解決此問題。使用協議緩衝區,您可以編寫要儲存的資料結構的.proto描述。由此,協議緩衝區編譯器建立了一個類,該類以有效的二進位制格式實現協議緩衝區資料的自動編碼和解析。生成的類為構成協議緩衝區的欄位提供獲取器和設定器,並以協議為單位來處理讀寫協議緩衝區的詳細資訊。重要的是,協議緩衝區格式支援隨時間擴充套件格式的想法,以使程式碼仍可以讀取以舊格式編碼的資料。

在哪裡找到示例程式碼?

我們的示例是一個命令列應用程式,用於管理使用協議緩衝區編碼的地址簿資料檔案。 命令AddressBook(請參閱:Program.cs)可以將新條目新增到資料檔案或解析資料檔案並將資料列印到控制檯。

您可以在GitHub儲存庫的examples目錄csharp / src / AddressBook目錄中找到完整的示例。

定義協議格式

要建立地址簿應用程式,您需要以.proto檔案開頭。 .proto檔案中的定義很簡單:您為要序列化的每個資料結構新增一條訊息,然後為訊息中的每個欄位指定名稱和型別。 在我們的示例中,定義訊息的.proto檔案是addressbook.proto

.proto檔案以程式包宣告開頭,這有助於防止不同專案之間的命名衝突。

syntax = "proto3";
package tutorial;

import "google/protobuf/timestamp.proto";

在C#中,如果未指定csharp_namespace,則將生成的類放置在與程式包名稱匹配的名稱空間中。 在我們的示例中,指定了csharp_namespace選項以覆蓋預設值,因此生成的程式碼使用Google.Protobuf.Examples.AddressBook的名稱空間而不是Tutorial。

option csharp_namespace = "Google.Protobuf.Examples.AddressBook";

接下來,您將擁有訊息定義。 訊息只是包含一組型別欄位的彙總。 許多標準的簡單資料型別可用作欄位型別,包括bool,int32,float,double和string。 您還可以通過使用其他訊息型別作為欄位型別來為訊息新增更多的結構。

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;
}

在上面的示例中,Person訊息包含PhoneNumber訊息,而AddressBook訊息包含Person訊息。您甚至可以定義巢狀在其他訊息中的訊息型別-如您所見,PhoneNumber型別在Person內部定義。如果希望您的欄位之一具有預定義的值列表之一,也可以定義列舉型別-在這裡您要指定電話號碼可以是MOBILE,HOME或WORK之一。

每個元素上的“ = 1”,“ = 2”標記標識該欄位在二進位制編碼中使用的唯一“標記”。標籤編號1至15與較高的編號相比,編碼所需的位元組減少了一個位元組,因此,為了進行優化,您可以決定將這些標籤用於常用或重複的元素,而將標籤16和更高的標籤用於較少使用的可選元素。重複欄位中的每個元素都需要重新編碼標籤號,因此重複欄位是此優化的最佳候選者。

如果未設定欄位值,則使用預設值:數字型別為零,字串為空字串,布林值為false。對於嵌入式訊息,預設值始終是訊息的“預設例項”或“原型”,沒有設定任何欄位。呼叫訪問器以獲取尚未顯式設定的欄位的值將始終返回該欄位的預設值。

如果重複一個欄位,則該欄位可以重複任意次(包括零次)。重複值的順序將保留在協議緩衝區中。將重複欄位視為動態大小的陣列。

協議緩衝區語言指南中,您將找到有關編寫.proto檔案的完整指南-包括所有可能的欄位型別。但是,不要去尋找類似於類繼承的工具–協議緩衝區不能做到這一點。

編譯協議緩衝區

現在,您有了.proto,接下來需要做的是生成讀取和寫入AddressBook(以及Person和PhoneNumber)訊息所需的類。 為此,您需要在.proto上執行協議緩衝區編譯器協議:

  • 如果尚未安裝編譯器,請下載軟體包並按照自述檔案中的說明進行操作。
  • 現在執行編譯器,指定源目錄(應用程式的原始碼所在的位置;如果您不提供值,則使用當前目錄),目標目錄(您希望生成的程式碼進入的位置;通常與$相同) SRC_DIR),以及.proto的路徑。 在這種情況下,您將呼叫:
protoc -I=$SRC_DIR --csharp_out=$DST_DIR $SRC_DIR/addressbook.proto

因為需要C#程式碼,所以使用--csharp_out選項–其他受支援的語言也提供了類似的選項。
這將在您指定的目標目錄中生成Addressbook.cs。 要編譯此程式碼,您需要一個引用Google.Protobuf程式集的專案。

通訊錄類

生成Addressbook.cs提供了五種有用的型別:

  • 靜態地址簿類,其中包含有關協議緩衝區訊息的後設資料。
  • 具有隻讀People屬性的AddressBook類。
  • 具有“名稱”,“ ID”,“電子郵件”和“電話”屬性的Person類。
  • 一個PhoneNumber類,巢狀在靜態Person.Types類中。
  • 一個PhoneType列舉,也巢狀在Person.Types中。

您可以在《 C#生成的程式碼》指南中詳細瞭解確切生成的內容的詳細資訊,但是在大多數情況下,您可以將它們視為完全普通的C#型別。需要強調的一點是,對應於重複欄位的任何屬性都是隻讀的。您可以向集合中新增專案或從集合中刪除專案,但是不能用完全獨立的集合來替換它。重複欄位的收集型別始終為RepeatedField 。此型別類似於List ,但有一些額外的便捷方法,例如,在大學初始化程式中使用的Add過載接受專案集合。

這是一個如何建立Person例項的示例:

Person john = new Person
{
    Id = 1234,
    Name = "John Doe",
    Email = "jdoe@example.com",
    Phones = { new Person.Types.PhoneNumber { Number = "555-4321", Type = Person.Types.PhoneType.Home } }
};

請注意,在C#6中,可以使用static刪除Person.Types的醜陋之處:

// Add this to the other using directives
using static Google.Protobuf.Examples.AddressBook.Person.Types;
...
// The earlier Phones assignment can now be simplified to:
Phones = { new PhoneNumber { Number = "555-4321", Type = PhoneType.HOME } }

解析和序列化

使用協議緩衝區的全部目的是對資料進行序列化,以便可以在其他位置對其進行解析。 每個生成的類都有一個WriteTo(CodedOutputStream)方法,其中CodedOutputStream是協議緩衝區執行時庫中的類。 但是,通常您將使用一種擴充套件方法來寫入常規System.IO.Stream或將訊息轉換為位元組陣列或ByteString。 這些擴充套件訊息位於Google.Protobuf.MessageExtensions類中,因此,當您要序列化時,通常會希望對Google.Protobuf名稱空間使用using指令。 例如:

using Google.Protobuf;
...
Person john = ...; // Code as before
using (var output = File.Create("john.dat"))
{
    john.WriteTo(output);
}

解析也很簡單。 每個生成的類都有一個靜態的Parser屬性,該屬性返回該型別的MessageParser 。 反過來,它具有解析流,位元組陣列和ByteStrings的方法。 因此,要解析我們剛剛建立的檔案,我們可以使用:

Person john;
using (var input = File.OpenRead("john.dat"))
{
    john = Person.Parser.ParseFrom(input);
}

Github儲存庫中提供了使用這些訊息維護地址簿(新增新條目並列出現有條目)的完整示例程式。

擴充套件協議緩衝區

在釋出使用協議緩衝區的程式碼後早晚,您無疑會想要“改善”協議緩衝區的定義。如果您希望新的緩衝區向後相容,而舊的緩衝區向後相容,並且您幾乎肯定希望這樣做,那麼您需要遵循一些規則。在新版本的協議緩衝區中:

  • 您不得更改任何現有欄位的標籤號。
  • 您可以刪除欄位。
  • 您可以新增新欄位,但必須使用新的標籤號(即,該協議緩衝區中從未使用過的標籤號,即使刪除的欄位也從未使用過)。

(這些規則有一些例外,但很少使用。)

如果遵循這些規則,舊程式碼將很樂意閱讀新訊息,而忽略任何新欄位。對於舊程式碼,刪除的單個欄位將僅具有其預設值,而刪除的重複欄位將為空。新程式碼還將透明地讀取舊訊息。

但是,請記住,新欄位不會出現在舊訊息中,因此您需要對預設值進行合理的處理。使用特定於型別的預設值:對於字串,預設值為空字串。對於布林值,預設值為false。對於數字型別,預設值為零。

反射

可以使用反射API以程式設計方式檢查訊息描述符(.proto檔案中的資訊)和訊息例項。 在編寫通用程式碼(例如不同的文字格式或智慧差異工具)時,此功能很有用。 每個生成的類都有一個靜態的Descriptor屬性,並且可以使用IMessage.Descriptor屬性來檢索任何例項的描述符。 作為如何使用它們的一個快速示例,這是一種列印任何訊息的頂級欄位的簡短方法。

public void PrintMessage(IMessage message)
{
    var descriptor = message.Descriptor;
    foreach (var field in descriptor.Fields.InDeclarationOrder())
    {
        Console.WriteLine(
            "Field {0} ({1}): {2}",
            field.FieldNumber,
            field.Name,
            field.Accessor.GetValue(message);
    }
}

參考文件

相關文章