@이광석 님의 패킷 라이브러리에서
안녕하세요.
개인적으로 쓸 일이 있어서 만들어보았는데
Nuget Library에 생각보다 별로 없는거 같아서 올려보았습니다.
Nuget 패키지명… 이름 오타여서 어떻게해야하나 고민했지만…
이것 또한 컨셉이라 생각하고 냅두기로 하였습니다.
다음은 패키지에서 제공하는 기능에 대한 간략한 설명입니다.
Chain형식의 Append 방식 기능 제공
PacketWriter writer = new PacketWriter()
.Append(0x01)
.Append(1)
.Append(1, false)
.Append(“ABC”)
.Append(“abc”);
var builder = new PacketBuilder ()
.Append(0x40)
.Append(0x41)
.Append(0x42)
.Append(0x43)
.Append(0x44)
…
소스 생성기를 이용한 패킷 직렬화 / 역직렬화 관련 기여를 고민하던 차에
저는 일감으로 소스 생성기를 이용해 ToPacket() 및 FromPacket() 메서드를 생성해주는 걸 해봐야겠습니다. 괜찮으시죠?
제가 제안한 내용이 지지가 없는 것이 아쉬워
Append 관련 다른 의견으로…
이렇게 사용하는 것은 어떨까 하는데요, 한번 보시죠.
var builder = new PacketBuilder()
.@byte (0x41)
.@bytes [0x42, 0x43, 0x44] // 또는 bytes (0x42, 0x43, 0x44)
.@int (312)
.@long (312312312)
.@string ("Test")
.Build();
흠…
var builder = new PacketBuilder()
[0x01]
[0x02, 0x03]
[0x04]
.Build();
처럼 너무 갔나요? ^^;
보기에는 이쁜데… ㅎㅎ
저는 2번입니다. 다만… @Byte 보다는 @byte가 좋아요 ^^
제 의견은 이렇습니다.
내용 증명의 목적으로 코딩을 해보고 있습니다.
중요 컨셉은 Schema
를 다음과 같이 정의하면
[GenPackable]
public partial class TestPacket
{
public readonly static PacketSchema Schema = PacketSchemaBuilder.Create(UnitEndian.Little, StringEncoding.ASCII)
.@byte(name: "Command", description: "명령어")
.BeginChecksum()
.@byte("Value1")
.@short("Value2")
.@string("Value3", "a", 5)
.EndChecksum()
.@checkum(ChecksumType.Sum8)
.Build();
}
다음의 코드가 생성됩니다.
public partial class TestPacket : GenPack.IGenPackable
{
/// <summary>
/// 명령어
/// </summary>
public byte Command { get; set; }
public byte Value1 { get; set; }
public short Value2 { get; set; }
/// <summary>
/// a
/// </summary>
public string Value3 { get; set; }
}
현재 소스 생성기를 이용해 위의 코드까지 생성하는 것을 해봤고 나아가 다음의 직렬화/역직렬화 메서드를 자동 생성하는 것입니다.
public partial class TestPacket : GenPack.IGenPackable
{
....
public static TestPacket FromPacket(Stream s)
{
....
}
public void ToPacket(Stream s)
{
....
}
}
소스 생성기의 장점은 단순한 규칙의 변경이 컴파일 타임 때 소스코드에 적용되어 컴파일 되는 점 같습니다. (그런데… 소스 생성기의 Context를 다루는 것은 생각보다 많이 힘드네요)
본 슬로그는 PacketByte Support에 소스 생성기를 이용한 직렬/역직렬화 기능을 PR 하는 것을 목표로 진행하려고 합니다.
13개의 좋아요
@object
, @list
, @dict
를 추가했습니다. 이제 다음의 스키마는 다음의 패킷(클래스)을 생성합니다.
[GenPackable]
public partial class TestPacket
{
public readonly static PacketSchema Schema = PacketSchemaBuilder.Create(UnitEndian.Little, StringEncoding.ASCII)
.@object<Test2Packet>("Test2Packet", "개체 포함")
.@list<Test2Packet>("Test2PacketList", "개체 리스트")
.@dict<Test2Packet>("Test2PacketDict", "개체 딕셔너리")
.@byte(name: "Command", description: "명령어")
.BeginPointChecksum()
.@byte("Value1")
.@short("Value2")
.@string("Value3", "a", 5)
.EndPointChecksum()
.@checkum(ChecksumType.Sum8)
.Build();
}
| 생성
public partial class TestPacket : GenPack.IGenPackable
{
/// <summary>
/// 개체 포함
/// </summary>
public GenPack.Test.Console.Test2Packet Test2Packet { get; set; }
/// <summary>
/// 개체 리스트
/// </summary>
public System.Collections.Generic.IList<GenPack.Test.Console.Test2Packet> Test2PacketList { get; set; } = new List<GenPack.Test.Console.Test2Packet>();
/// <summary>
/// 개체 딕셔너리
/// </summary>
public System.Collections.Generic.IDictionary<string, GenPack.Test.Console.Test2Packet> Test2PacketDict { get; set; } = new Dictionary<string, GenPack.Test.Console.Test2Packet>();
/// <summary>
/// 명령어
/// </summary>
public byte Command { get; set; }
public byte Value1 { get; set; }
public short Value2 { get; set; }
/// <summary>
/// a
/// </summary>
public string Value3 { get; set; } = string.Empty;
}
1개의 좋아요
ToPacket()
을 자동 생성하는 코드를 구현하였습니다. (아직 검증은 하지 못했습니다.)
워낙 소스 생성기의 경험이 부족해서 동작성을 확보 후 올바른 방향으로 코드를 조금씩 수정해야 할 것 같습니다.
[GenPackable]
public partial class TestPacket
{
public readonly static PacketSchema Schema = PacketSchemaBuilder.Create(UnitEndian.Little, StringEncoding.ASCII)
.@array<byte>("ByteArray", 50, "byte 배열")
.@array<int>("IntegerArray", 50, "int 배열")
.@array<Test2Packet>("Test2PacketArray", 5, "개체 배열")
.@object<Test2Packet>("Test2Packet", "개체 포함")
.@list<Test2Packet>("Test2PacketList", "개체 리스트")
.@dict<Test2Packet>("Test2PacketDict", "개체 딕셔너리")
.@byte(name: "Command", description: "명령어")
.BeginPointChecksum()
.@byte("Value1")
.@short("Value2")
.@string("Value3", "a", 5)
.EndPointChecksum()
.@checkum(ChecksumType.Sum8)
.Build();
}
| 생성 코드
public partial class TestPacket : GenPack.IGenPackable
{
/// <summary>
/// byte 배열
/// </summary>
public byte[] ByteArray { get; } = new byte[50];
/// <summary>
/// int 배열
/// </summary>
public int[] IntegerArray { get; } = new int[50];
/// <summary>
/// 개체 배열
/// </summary>
public GenPack.Test.Console.Test2Packet[] Test2PacketArray { get; } = new GenPack.Test.Console.Test2Packet[5];
/// <summary>
/// 개체 포함
/// </summary>
public GenPack.Test.Console.Test2Packet Test2Packet { get; set; }
/// <summary>
/// 개체 리스트
/// </summary>
public System.Collections.Generic.IList<GenPack.Test.Console.Test2Packet> Test2PacketList { get; } = new List<GenPack.Test.Console.Test2Packet>();
/// <summary>
/// 개체 딕셔너리
/// </summary>
public System.Collections.Generic.IDictionary<string, GenPack.Test.Console.Test2Packet> Test2PacketDict { get; } = new Dictionary<string, GenPack.Test.Console.Test2Packet>();
/// <summary>
/// 명령어
/// </summary>
public byte Command { get; set; }
public byte Value1 { get; set; }
public short Value2 { get; set; }
/// <summary>
/// a
/// </summary>
public string Value3 { get; set; } = string.Empty;
public void ToPacket(System.IO.Stream stream)
{
System.IO.BinaryWriter writer = new System.IO.BinaryWriter(stream);
writer.Write(ByteArray);
foreach (var item in IntegerArray)
{
writer.Write(item);
}
foreach (var item in Test2PacketArray)
{
item.ToPacket(stream);
}
Test2Packet.ToPacket(stream);
writer.Write(Test2PacketList.Count);
foreach (var item in Test2PacketList)
{
item.ToPacket(stream);
}
writer.Write(Test2PacketDict.Count);
foreach (var item in Test2PacketDict)
{
writer.Write(item.Key);
item.Value.ToPacket(stream);
}
writer.Write(Command);
writer.Write(Value1);
writer.Write(Value2);
writer.Write(Value3);
}
}
엔디안 및 문자열 인코딩 부분(및 체크섬)은 아직 구현하지 못했는데 BinaryWriter
가 little endian
만 지원하는군요. 일단 엔디안은 무시하고 모두 구현한 후 마지막으로 구현해야겠습니다.
2개의 좋아요
FromPacket()
메서드를 구현하였습니다. 검증을 위해 xUnit 프로젝트를 추가했고 하나씩 테스트를 하려고 합니다.
[GenPackable]
public partial class TestPacket
{
public readonly static PacketSchema Schema = PacketSchemaBuilder.Create(UnitEndian.Little, StringEncoding.ASCII)
.@array<byte>("ByteArray", 50, "byte 배열")
.@array<int>("IntegerArray", 50, "int 배열")
.@array<Test2Packet>("Test2PacketArray", 5, "개체 배열")
.@object<Test2Packet>("Test2Packet", "개체 포함")
.@list<Test2Packet>("Test2PacketList", "개체 리스트")
.@dict<Test2Packet>("Test2PacketDict", "개체 딕셔너리")
.@byte(name: "Command", description: "명령어")
.BeginPointChecksum()
.@byte("Value1")
.@short("Value2")
.@string("Value3", "a", 5)
.EndPointChecksum()
.@checkum(ChecksumType.Sum8)
.Build();
}
| 생성
public partial class TestPacket : GenPack.IGenPackable
{
/// <summary>
/// byte 배열
/// </summary>
public byte[] ByteArray { get; } = new byte[50];
/// <summary>
/// int 배열
/// </summary>
public int[] IntegerArray { get; } = new int[50];
/// <summary>
/// 개체 배열
/// </summary>
public GenPack.Test.Console.Test2Packet[] Test2PacketArray { get; } = new GenPack.Test.Console.Test2Packet[5];
/// <summary>
/// 개체 포함
/// </summary>
public GenPack.Test.Console.Test2Packet Test2Packet { get; set; }
/// <summary>
/// 개체 리스트
/// </summary>
public System.Collections.Generic.IList<GenPack.Test.Console.Test2Packet> Test2PacketList { get; } = new List<GenPack.Test.Console.Test2Packet>();
/// <summary>
/// 개체 딕셔너리
/// </summary>
public System.Collections.Generic.IDictionary<string, GenPack.Test.Console.Test2Packet> Test2PacketDict { get; } = new Dictionary<string, GenPack.Test.Console.Test2Packet>();
/// <summary>
/// 명령어
/// </summary>
public byte Command { get; set; }
public byte Value1 { get; set; }
public short Value2 { get; set; }
/// <summary>
/// a
/// </summary>
public string Value3 { get; set; } = string.Empty;
public byte[] ToPacket()
{
using var ms = new System.IO.MemoryStream();
ToPacket(ms);
return ms.ToArray();
}
public void ToPacket(System.IO.Stream stream)
{
System.IO.BinaryWriter writer = new System.IO.BinaryWriter(stream);
writer.Write(ByteArray);
foreach (var item in IntegerArray)
{
writer.Write(item);
}
foreach (var item in Test2PacketArray)
{
item.ToPacket(stream);
}
Test2Packet.ToPacket(stream);
writer.Write(Test2PacketList.Count);
foreach (var item in Test2PacketList)
{
item.ToPacket(stream);
}
writer.Write(Test2PacketDict.Count);
foreach (var item in Test2PacketDict)
{
writer.Write(item.Key);
item.Value.ToPacket(stream);
}
writer.Write(Command);
writer.Write(Value1);
writer.Write(Value2);
writer.Write(Value3);
}
public static TestPacket FromPacket(byte[] data)
{
using var ms = new System.IO.MemoryStream(data);
return FromPacket(ms);
}
public static TestPacket FromPacket(System.IO.Stream stream)
{
TestPacket result = new TestPacket();
System.IO.BinaryReader reader = new System.IO.BinaryReader(stream);
int size = 0;
byte[] buffer = null;
buffer = reader.ReadBytes(50);
Array.Copy(buffer, result.ByteArray, 50);
size = reader.ReadInt32();
for (var i = 0; i < size; i++)
{
result.IntegerArray[i] = reader.ReadInt32();
}
size = reader.ReadInt32();
for (var i = 0; i < size; i++)
{
result.Test2PacketArray[i] = GenPack.Test.Console.Test2Packet.FromPacket(stream);
}
result.Test2Packet = GenPack.Test.Console.Test2Packet.FromPacket(stream);
size = reader.ReadInt32();
for (var i = 0; i < size; i++)
{
result.Test2PacketList.Add(GenPack.Test.Console.Test2Packet.FromPacket(stream));
}
size = reader.ReadInt32();
for (var i = 0; i < size; i++)
{
result.Test2PacketDict[reader.ReadString()] = GenPack.Test.Console.Test2Packet.FromPacket(stream);
}
result.Command = reader.ReadByte();
result.Value1 = reader.ReadByte();
result.Value2 = reader.ReadInt16();
result.Value3 = reader.ReadString();
return result;
}
}
BinaryReader.Read(Span<byte>)
의 경우 .NET Stnadard 2.0
에서 지원하지 않아 타켓 프레임워크를 식별하는 방법을 찾아 분기해야 겠습니다.
2개의 좋아요
속성이 하나만 있는 패킷으로 단위 테스트를 진행했고 BinaryWriter
의 Write(string)
에서 7bit 가변 길이
를 사용함을 알 수 있었습니다. 현재 list 및 dict는 4바이트 고정 길이를 사용하므로 1차 목표는 7bit 가변 길이
를 동일하게 list, dict에 적용하고 차후에 길이 저장 시 8비트, 16비트 32비트, 가변
을 선택할 수 있도록 수정해야 겠습니다.
3개의 좋아요
BinaryWriter.Write(string)
에서 사용하는 가변 7bit 사이즈 저장 방식은 BinaryWriter. Write7BitEncodedInt(size)
를 이용해서 저장됩니다. @list , @dict 저장 시 이것을 이용할 수 있었습니다.
구현 방식이 궁금해서 살펴봤는데,
public void Write7BitEncodedInt(int value)
{
uint uValue = (uint)value;
while (uValue > 0x7Fu)
{
Write((byte)(uValue | ~0x7Fu));
uValue >>= 7;
}
Write((byte)uValue);
}
꽤 심플한 코드네요. 원리는 길이를 바이트 단위로 비교해서 0x7F
보다 클 경우 최상위 비트를 1로 만들고 계속해서 7bit 씩 쉬프트 하는 방식입니다.
3개의 좋아요
0.9 미리보기 1
버전을 릴리스 하였습니다.
아… 원래 라이브러리를 만들 계획은 없었고 @이광석 님의 패킷 관련 라이브러리인 PacketByte Support의 직렬/역직렬의 소스생성기 버전을 PR하기 위해 학습용으로 만들다가 Microsoft MVP 갱신 날과 맞물려 기간 동안의 오프라인 활동이 없어 갱신이 안될까 쫄려서 급조(?)한 프로젝트 입니다.
갑자기 만든 프로젝트이지만 만들다 보니 옛날 생각도 나고 욕심도 생겨서 앞으로 구현할 목록도 나름 계획하게 되었습니다…
기본 사용법은 깃허브 방문 하시면 확인할 수 있고요, 미리보기로 0.9-preview1을 설치해서 동작성을 확인할 수 있습니다.
이제 @이광석 님의 라이브러리에 소스 생성기로 기여할 수 있을 정도가 된 것 같습니다.
PacketByte Support
의 경우 일반 class
를 대상으로 해야 하므로 System.Text.Json
과 유사한 전략을 사용해야 합니다.
[JsonSourceGenerationOptions(WriteIndented = true)]
[JsonSerializable(typeof(WeatherForecast))]
internal partial class SourceGenerationContext : JsonSerializerContext
{
}
JsonSerializable(typeof(WeatherForecast))
특성을 주어서 SourceGenerationContext
에WeatherForecast
를 직렬화/역직렬화 하는 코드를 자동 생성하는 식입니다.
6개의 좋아요
갑자기 든 생각인데 패킷은 이전 패킷과의 호환성의 이유로 리비전 관리가 필요합니다.
이를 고려한 것이 MemoryPack의 경우 MemoryPackOrder
특성입니다.
게임을 만든다고 할 때 버전1의 플레이어는 배고픔이 없었다가 버전2에서 배고픔이 생겼다고 칩시다.
플레이어에 배고픔
속성이 추가되었고 이때부터 과거 플레이어 패킷과의 호환성이 깨지게 됩니다. (읽을 수 없게 됩니다)
이를 고려하여 리비전이라는 개념을 둬서,
.@ver(2)
...
.@int(revision: 2, "배고픔", "배고픔을 나타내며 0 ~ 100의 값")
@ver
로 기록된 버전을 식별하여 r2에서 r1의 데이터를 읽을 수 있게 만들 수 있을 것 같습니다.
4개의 좋아요