本文使用協議緩沖區語言的proto3版本,為C#程序員提供了使用協議緩沖區的基本介紹。 通過創建一個簡單的示例應用程序,展示了如何
- 在.proto文件中定義消息格式。
- 使用協議緩沖區編譯器。
- 使用C#協議緩沖區API寫入和讀取消息。
這不是在C#中使用協議緩沖區的全面指南。 有關更多詳細的參考信息,請參閱《協議緩沖區語言指南》,《 C#API參考》,《 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
這是一個如何創建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
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);
}
}
參考文檔