Protobuf language (with C#)
Peponi │ 11/25/2024 │ 11m
Protobuf language (with C#)
Peponi
1. Introduction
이 문서에서는 proto3 문법 및 C#에서의 사용 방법을 알아본다.
2. Proto definitions
// Protocol version 지정
syntax = "proto3";
// Package name 지정
package ProtoTest;
// C# namespace 지정 옵션
// 옵션을 지정하지 않은 경우 package name이 namespace로 지정됨
option csharp_namespace = "MyProto";
// 다른 proto import
import "ProtoTest.Transaction.proto";package 또는 csharp_namespace로 namespace를 지정하는 방법은 C#과 동일하다.
2.1. import public
import public을 선언 후 다른 곳에서 해당 proto를 import하면 import public 또한 같이 import 된다.
// contents...// All proto files that import this proto will be import under proto also.
import public "A.proto";import "B.proto";
// Use definitions from 'B.proto' and 'A.proto'3. Scalar value types
Protobuf는 자체 type을 가지고 있으며, C# 언어에 대응하는 형식은 아래 표를 참조한다.
| Protobuf | C# | Note |
|---|---|---|
| double | double | |
| float | float | |
| int32 | int | Uses variable-length encoding. Inefficient for encoding negative numbers – if your field is likely to have negative values, use sint32 instead. |
| int64 | long | Uses variable-length encoding. Inefficient for encoding negative numbers – if your field is likely to have negative values, use sint64 instead. |
| uint32 | uint | Uses variable-length encoding. |
| uint64 | ulong | Uses variable-length encoding. |
| sint32 | int | Uses variable-length encoding. Signed int value. These more efficiently encode negative numbers than regular int32s. |
| sint64 | long | Uses variable-length encoding. Signed int value. These more efficiently encode negative numbers than regular int64s. |
| fixed32 | uint | Always four bytes. More efficient than uint32 if values are often greater than 2<sup>28</sup>. |
| fixed64 | ulong | Always eight bytes. More efficient than uint64 if values are often greater than 2<sup>56</sup>. |
| sfixed32 | int | Always four bytes. |
| sfixed64 | long | Always eight bytes. |
| bool | bool | |
| string | string | A string must always contain UTF-8 encoded or 7-bit ASCII text, and cannot be longer than 2<sup>32</sup>. |
| bytes | ByteString | May contain any arbitrary sequence of bytes no longer than 2<sup>32</sup>. |
다른 언어에 대한 내용은 Scalar Value Types을 참조한다.
4. Language
4.1. Message
메시지는 필드를 담는 객체이며 gRPC 통신에 사용되기도 한다. C#의 class와 비슷하다. 메시지 안에 들어있는 필드는 name/value pair로 구성되며 Value 값의 범위는 1 ~ 536,870,911 이며 다음 제약을 가진다.
- Value 값은 한 메시지 내에서 고유하다.
19,000~19,999값은 Protocol buffer 정의에 의해 예약되어 있다.- Reserved field 또는 Extension에 정의된 값은 사용할 수 없다.
message Transaction
{
string SenderAddress = 1;
int32 SenderPort = 2;
int32 TransactionID = 3;
}
message Message
{
Transaction Transaction = 1;
string Message = 2;
}message Outer
{
message Inner
{
int32 IntValue = 1 ;
}
Inner InnerValue = 1;
}
message Other
{
Outer.Inner InnerValue = 1;
}필드에는 다음과 같은 레이블을 추가할 수 있다.
message Optional
{
optional int32 TestInt = 1;
}
message Repeated
{
repeated int32 TestInts = 1;
}
message Map
{
map<string, int32> TestPairs = 1;
}optional- 필드에 값이 할당된 경우 : 일반 필드와 같이 사용한다. wire로 serialize 될 수 있다.
- 필드에 값이 할당되지 않은 경우 : 기본값을 반환하며 wire로 serialize 될 수 없다.
repeatedrepeated레이블이 지정된 필드는 enumerator type으로 동작한다.- IList<T>를 구현하여 LINQ 쿼리 사용 및 배열, 리스트로 변환이 가능하다.
mapmap레이블이 지정된 필드는 key/value pair type으로 동작한다.- IDictionary<TKey,TValue>를 구현하여 LINQ 쿼리가 가능하다.
4.2. Message 사용 방법
private static void Main(string[] args)
{
Optional opt = new Optional();
Console.WriteLine($"Has value : {opt.HasTestInt}, value : {opt.TestInt}");
Repeated rpt = new Repeated();
rpt.TestInts.Add(1);
rpt.TestInts.Add(2);
Console.WriteLine(rpt);
Map map = new Map();
map.TestPairs.Add("A", 1);
map.TestPairs.Add("B", 2);
Console.WriteLine(map);
}
/* output:
Has value : False, value : 0
{ "TestInts": [ 1, 2 ] }
{ "TestPairs": { "A": 1, "B": 2 } }
*/4.3. Reserved field
Reserved field는 key 또는 value를 점유하여 해당 값을 사용하지 못하게 한다. 향후 추가될 필드를 미리 정의하거나 메시지에서 제거된 필드를 사용하지 못하게 하는 데 활용할 수 있다.
message Reserved
{
reserved 1, 2, 3 to 5;
reserved "NameA", "NameB";
// Following lines will be occurring error by protobuf compiler
int32 TestInt = 2;
int32 NameA = 6;
}4.4. Oneof
oneof는 message field를 구성하는 기능 중 하나이다. oneof에 등록된 어떤 형식이라도 멤버들 중 하나만 값을 가질 수 있으며 다음 제약 조건이 있다.
oneof멤버의 형식에map은 사용이 불가하다.oneof는repeated로 지정할 수 없다.
Protobuf 컴파일러는 컴파일 시 oneof 키워드를 처리하여 enum을 생성해준다. 이를 이용하여 C# 코드에서 설정된 필드가 무엇인지 찾을 수 있다.
message OneOf
{
oneof Selected{
string A = 1;
int32 B = 2;
bool C = 3;
}
}private static void Main(string[] args)
{
OneOf oneOf = new OneOf();
oneOf.B = 1010;
CheckOneof(oneOf);
}
private static void CheckOneof(OneOf oneOf)
{
switch (oneOf.SelectedCase)
{
case OneOf.SelectedOneofCase.A:
Console.WriteLine(oneOf.A);
break;
case OneOf.SelectedOneofCase.B:
Console.WriteLine(oneOf.B);
break;
case OneOf.SelectedOneofCase.C:
Console.WriteLine(oneOf.C);
break;
default:
Console.WriteLine("None");
break;
}
}
/* output:
1010
*/4.5. Comments
Protobuf의 주석 작성법은 C#과 동일하다.
// Comments in single line
/* Comments in
Multiple line */4.6. Enumerations
enum NoticeType
{
None = 0;
UserNotify = 1;
SystemMessage = 2;
Warn = 3;
Error = 4;
Exception = 5;
}
message Notice
{
NoticeType Type = 1;
string Message = 2;
}Protobuf의 enum 작성법은 C#과 동일하다. enum의 제약사항으로 첫번째 항목은 반드시 0으로 지정되어야 한다.
4.6. Reserved value
Reserved value는 key 또는 value를 점유하여 해당 값을 사용하지 못하게 한다. 향후 추가될 값을 미리 정의하거나 enum에서 제거된 값을 사용하지 못하게 하는 데 활용할 수 있다.
enum Reserved
{
reserved 1, 2, 3 to 5;
reserved "NameA", "NameB";
}4.7. Any
Any는 재사용 가능한 형식으로, C#의 object와 비슷한 면이 있다. google/protobuf/any.proto를 import 해주어야 하며 Pack(IMessage), Unpack<T>(), TryUnpack<T>(out T), Is(MessageDescriptor) 등의 메서드를 지원하여 형변환 및 확인이 가능하다.
import "google/protobuf/any.proto";
package ProtoTest;
message Foo { }
message Bar { }
message Baz
{
google.protobuf.Any Data = 1;
}using Google.Protobuf.WellKnownTypes;
using ProtoTest;
private static void Main(string[] args)
{
Baz baz = new Baz();
baz.Data = Any.Pack(new Foo());
CheckType(baz);
}
private static void CheckType(Baz baz)
{
if (baz.Data.Is(Foo.Descriptor))
{
Console.WriteLine("Foo");
}
else if (baz.Data.Is(Bar.Descriptor))
{
Console.WriteLine("Bar");
}
else
{
throw new ArgumentException($"{baz.Data} is unknown type");
}
}
/* output:
Foo
*/4.8. Service
service를 정의하기 위해 Grpc Nuget 패키지를 설치한다. service는 RPC service (WCF, gRPC 등...) 에 message를 사용 가능하게 해준다. Protobuf compiler는 service 선언을 확인하여 interface code 및 stub을 생성한다. 아래는 생성한 service를 이용한 gRPC 통신의 예시다.
package ProtoTest;
message Transaction
{
string SenderAddress = 1;
int32 SenderPort = 2;
int32 TransactionID = 3;
}
message Message
{
Transaction Transaction = 1;
string Message = 2;
}
service CoreService
{
rpc Ping(Message) returns (Message);
}using ProtoTest;
using Grpc.Core;
private static void Main(string[] args)
{
// RPC channel 생성
Channel channel = new Channel("localhost", 50001, SslCredentials.Insecure);
// Service client 생성
var coreService = new CoreService.CoreServiceClient(channel);
Transaction transaction = new Transaction() { SenderAddress = "127.0.0.1", SenderPort = 50001, TransactionID = 0 };
Message message = new Message() { Transaction = transaction, Message_ = "Hello" };
// Server에 Ping service 요청
var returnMessage = coreService.Ping(message);
Console.WriteLine(returnMessage);
}IMPORTANT
gRPC 통신을 하는 경우 Protocol buffer 요소는 통신을 위한 인터페이스이기 때문에 매우 중요하다. 서버와 클라이언트 개발의 주체가 다른 경우에는 특히 신중하게 다뤄야 한다.
- Wire 형식이 달라지는 경우, 서버측에서 변경된
*.proto파일을 제공하거나 알려주지 않으면 클라이언트 측에서는 이를 알기 어렵다.
외국의 서버와 통신하는 과정에서 이런 일이 발생하였는데, 업데이트 시점마다 달라지는 인터페이스로 인해 고생한 일이 있었다. (연락이 잘 되지 않아 매번 통신 테스트를 하며 변경사항이 있는지 일일이 확인해야 했다.)enum의 key, value의 순서 또는 값을 변경하는 경우도 마찬가지다.enum을 제어 요소로 활용하는 경우 클라이언트 측에서는 변경된 요소에 대해 알지 못해 어리둥절한 상황이 발생할 수 있다.
- 메시지에 대해 주의해야 하는 사항에 대한 공식 자료는 다음을 참조한다.