gRPC-Protocol基礎知識-C#篇

本文使用協議緩衝區語言的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 = "[email protected]",
    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);
    }
}

參考文檔