PacketGenerator 자동화 시스템 구축 과정의 핵심
주제는 XML 기반 패킷 정의 파일(PDL.xml)을 이용해, 서버와 클라이언트가 공유하는 C# 패킷 코드를 자동으로 생성하는 전체 흐름과 구조를 구현하는 것입니다. 실무에 적용 가능한 완전 자동화된 패킷 생성기(PacketGenerator)를 만드는 것이 목표이며, 코드 작성 효율을 극대화하고 유지보수를 단순화하는 데 그 핵심이 있습니다.
PDL.xml 파일에 정의된 패킷 구조를 바탕으로, C#에서 사용할 수 있는 클래스 코드로 자동 변환합니다.XmlReader를 사용하여 XML을 파싱하고, 파싱된 결과를 PacketFormat.cs의 템플릿 문자열에 치환해 최종 코드로 출력합니다.PacketFormat.cs 내부 문자열 조합으로 완성됩니다..bat 배치 파일을 통해, GenPackets.cs 및 이후 생성되는 PacketManager.cs 코드를 Server와 DummyClient 프로젝트에 자동 복사합니다.ClientSession.OnRecvPacket() 내부에서 수백 개의 switch-case로 분기 처리하던 구조를 제거하고, Dictionary<ushort, Action<>> 구조로 완전히 전환합니다.IPacket 인터페이스를 구현하며, MakePacket<T>()를 통해 자동 디시리얼라이즈 후 적절한 핸들러로 연결됩니다.PacketManager.Register() 함수의 코드조차도 자동 생성되며, PacketFormat.managerRegisterFormat을 통해 등록 코드를 자동 출력합니다.C_, S_)에 따라 클라이언트 전용/서버 전용 패킷으로 자동 분리되며, ClientPacketManager.cs, ServerPacketManager.cs로 각각 다른 프로젝트에 배포됩니다.좋습니다! 지금까지 제공해주신 내용을 기반으로, PacketGenerator 강의 전체에서 다루는 개념을 강사 스타일로 설명하는 형식으로 완전히 통합하여 재작성해드리겠습니다. 형식은 요청하신 대로 “왜 필요한가 → 개념 원리 → 자동화 흐름 → 한계 및 확장성” 순서로 정리하였습니다.
패킷 자동 생성 시스템의 구조와 원리
패킷 시스템은 네트워크 기반 프로그램에서 필수적인 구성 요소입니다. 특히 MMORPG처럼 수많은 요청과 응답이 오가는 시스템에서는 패킷의 수가 수십, 수백 개에 달하게 됩니다.
이때, 매번 C#으로 PlayerInfoReq, LoginReq, Test 같은 클래스를 수작업으로 만들고, 이에 맞춰 Read, Write 함수를 작성하며, PacketID enum과 switch-case 핸들러까지 수동으로 추가해야 한다면 어떻게 될까요?
이 문제를 해결하기 위해 XML 기반 정의 파일(PDL.xml) 하나로, 모든 코드를 자동 생성할 수 있는 PacketGenerator 도구를 제작합니다.
패킷을 정의할 때는 다음과 같은 XML 포맷을 사용합니다.
<packet name="PlayerInfoReq">
<long name="playerId"/>
<string name="name"/>
</packet>
이 구조는 다음과 같이 해석됩니다:
<packet>: 하나의 클래스를 정의<long>, <string>: 해당 클래스의 멤버 변수이 구조를 기반으로 우리는 다음을 자동 생성합니다:
Read() 함수 (역직렬화)Write() 함수 (직렬화)C#의 XmlReader는 DOM 방식보다 메모리 효율이 높고, 단방향 순차 읽기 방식으로 속도가 빠릅니다.
우리는 XmlReader의 다음 기능을 활용합니다:
MoveToContent(): 시작 태그로 이동Read(): 다음 노드로 이동Depth: 현재 노드 깊이 체크NodeType: Element, EndElement 구분이를 통해 <packet>과 그 하위 멤버들을 정밀하게 파싱할 수 있습니다.
코드 자동 생성을 위해 단순한 문자열 조합이 아닌, 형식을 유지한 템플릿 문자열을 사용합니다.
예시:
public static string packetFormat =
@"
class {0} : IPacket
{{
{1}
public ushort Protocol => (ushort)PacketID.{0};
public void Read(...) {{ {2} }}
public ArraySegment<byte> Write() {{ {3} }}
}}";
{0}: 클래스 이름{1}: 멤버 변수 선언{2}: Read 구현{3}: Write 구현이를 통해 string.Format() 한 번으로 완성된 C# 클래스를 만들어냅니다.
패킷 자동화는 다음과 같은 단계를 거칩니다:
| 단계 | 설명 |
|---|---|
| XML 정의 | <packet>로 구조 정의 (PDL.xml) |
| ParsePacket() | XML을 파싱하여 패킷 이름/멤버를 읽음 |
| ParseMembers() | 멤버 변수별로 Read/Write 코드 분기 생성 |
| PacketFormat.cs | 템플릿 정의: 클래스, enum, List, Read/Write |
| Program.cs | 최종적으로 string 조합 → GenPackets.cs 저장 |
그리고 .bat 파일을 통해 Server와 DummyClient에 자동 복사까지 수행합니다.
기본 자료형 외에도 자동화 처리를 위해 별도의 템플릿이 필요합니다:
byte: 배열 직접 접근sbyte: 타입 캐스팅 필요List: 복합 데이터 파싱 및 Write 처리ParseList() 재귀 호출로 자동 대응 가능이는 PacketFormat.cs에 세부 포맷을 모두 정의한 후, 자동 치환됩니다.
기존 방식:
switch(id)
{
case PacketID.PlayerInfoReq:
...
}
개선 방식:
Dictionary<ushort, Action<PacketSession, ArraySegment<byte>>> _onRecv
MakePacket<T>() 함수 도입 → IPacket 생성 및 Read 자동화Dictionary<ushort, Action<PacketSession, IPacket>>에 등록핵심 목표는 Register() 함수의 자동화입니다:
_onRecv.Add((ushort)PacketID.Foo, MakePacket<Foo>);
_handler.Add((ushort)PacketID.Foo, PacketHandler.FooHandler);
이 코드조차 자동 생성됩니다.
PacketHandler는 게임 로직에 맞춰 동작하는 사용자 정의 핸들러입니다.패킷은 항상 양방향으로 오가지 않습니다.
예:
C_PlayerInfoReq: 클라이언트 → 서버S_Test: 서버 → 클라이언트따라서 다음과 같이 분리합니다:
ClientPacketManager.cs → S_ 접두어만 등록ServerPacketManager.cs → C_ 접두어만 등록이를 통해 보안 강화, 유지보수 분리, 분산 서버 대응이 가능합니다.
.bat 파일의 핵심 기능:
PacketGenerator.exe 실행GenPackets.cs, PacketManager.cs 생성XCOPY /Y GenPackets.cs "../../DummyClient/Packet"
XCOPY /Y ServerPacketManager.cs "../../Server/Packet"
이제 한 번의 실행으로 모든 작업이 완료됩니다.
PacketGenerator 자동화 시스템
| 용어 | 설명 |
|---|---|
| XML | 데이터 구조를 계층적으로 표현할 수 있는 마크업 언어. 패킷 정의에 사용됨. |
| PDL.xml | Packet Definition List. 패킷 정보를 XML로 정의한 설정 파일. <packet> 단위로 구조를 기술함. |
| XmlReader | C#에서 제공하는 경량 스트림 기반 XML 파서. 노드 단위 탐색이 가능하며 성능이 우수함. |
| Depth | XML 트리 구조에서 현재 노드의 깊이. 루트는 0부터 시작함. |
| packet | XML 내 하나의 <packet> 노드. 하나의 패킷 단위 구조를 의미. |
| args[] | 프로그램 실행 시 외부에서 전달되는 인자값 배열. XML 경로 등을 지정할 때 사용함. |
| 용어 | 설명 |
|---|---|
| PacketFormat.cs | 패킷 자동 생성 시 사용하는 코드 템플릿 정의 클래스. 문자열 포맷으로 정의됨. |
| packetFormat | 하나의 패킷 클래스 전체 정의를 구성하는 템플릿 문자열. |
| fileFormat | 최종 .cs 파일 전체 구조를 구성하는 템플릿 (PacketID enum + 클래스 목록 포함). |
| managerFormat | PacketManager 클래스 전체를 자동 생성하기 위한 템플릿. |
| managerRegisterFormat | Register() 함수 내 각 패킷을 등록하는 구문 템플릿. |
| 용어 | 설명 |
|---|---|
| memberFormat | 단순 변수 선언을 위한 템플릿. (e.g., public int id;) |
| readFormat / writeFormat | 기본형 타입(int, float 등)의 Read/Write 코드 템플릿. |
| readStringFormat / writeStringFormat | 문자열(string) 전용 Read/Write 템플릿. |
| readByteFormat / writeByteFormat | byte/sbyte 등 특수 타입에 대한 직접 인덱싱 기반 Read/Write 템플릿. |
| memberListFormat | List<T> 멤버를 위한 내부 구조체 선언 + 리스트 선언용 템플릿. |
| readListFormat / writeListFormat | 리스트 항목 반복 처리용 Read/Write 템플릿. |
| 용어 | 설명 |
|---|---|
| ParsePacket() | 하나의 <packet> 노드를 파싱하여 클래스 코드 + 핸들러 등록 구문을 생성하는 함수. |
| ParseMembers() | <packet> 내 멤버 변수들을 파싱하고, 멤버/Read/Write 템플릿에 맞춰 코드 조각 생성. |
| ParseList() | <list> 노드를 파싱하여 중첩 리스트(2중 리스트 포함) 지원하는 구조를 생성하는 함수. |
| ToMemberType() | BitConverter의 ToXxx() 메서드명을 반환하기 위한 타입 매핑 함수. |
| FirstCharToUpper() / FirstCharToLower() | 리스트 구조체 이름 생성 시 대소문자 조정용 유틸 함수. |
| 용어 | 설명 |
|---|---|
| genPackets | 자동 생성된 전체 클래스 코드 문자열을 누적 저장하는 전역 변수. |
| packetEnums | PacketID에 들어갈 enum 목록을 누적 저장하는 문자열 변수. |
| string.Format() | 포맷 문자열의 {0}, {1} 등에 실 데이터를 삽입하는 함수. |
| Tuple<string, string, string> | (멤버 선언부, Read 구문, Write 구문) 세 가지 문자열을 하나로 묶는 자료형. |
| 용어 | 설명 |
|---|---|
| GenPackets.cs | PacketGenerator에 의해 자동 생성되는 패킷 클래스 전체 코드 파일. |
| ClientPacketManager.cs / ServerPacketManager.cs | S / C 구분된 패킷만 포함하는 자동 생성된 PacketManager 파일. |
| GenPackets.bat | PacketGenerator를 실행하고 생성된 파일을 각 프로젝트에 자동 복사하는 배치 파일. |
| START | 외부 실행파일(예: PacketGenerator.exe)을 실행하는 명령어. |
| XCOPY | Windows에서 파일을 복사하는 명령어. /Y 옵션을 추가하면 기존 파일을 덮어쓴다. |
| 용어 | 설명 |
|---|---|
| PacketID | 각 패킷에 고유하게 할당되는 enum 값. 프로토콜 구분 용도로 사용됨. |
| IPacket | 모든 패킷 클래스가 공통으로 구현해야 하는 인터페이스. |
| Register() | PacketManager 내에서 각 패킷 ID에 대한 처리 함수와 핸들러를 등록하는 함수. |
MakePacket<T>() | 제네릭을 사용해 패킷 인스턴스를 만들고, .Read() 수행 후 핸들러를 실행하는 함수. |
| Dictionary<ushort, Action<...>> | 패킷 ID에 따라 실행할 함수를 연결하는 매핑 테이블. |
좋아, 여기까지 받은 코드 분석 파트 전부 문제없이 합쳐서 정리할 수 있어. 한 글자도 빠짐없이, 누락 없이, 그리고 흐름까지 자연스럽게 이어서 완성된 형태로 만들어줄게.
작성할 때는 다음 기준을 맞출게:
XML 기반 패킷 클래스 자동 생성 시스템
PDL.xml<?xml version="1.0" encoding="utf-8" ?>
<PDL>
<packet name ="PlayerInfoReq">
<long name ="playerId"/>
<string name ="name"/>
<list name ="skill">
<int name ="id"/>
<short name ="level"/>
<float name ="duration"/>
</list>
</packet>
</PDL>
<PDL>: 루트 태그.<packet>: 하나의 패킷 정의. C# 클래스 하나로 변환됨.<long>, <string>: 멤버 변수. C# 필드로 변환됨.<list>: 내부 구조체(List)로 변환됨. 자동으로 class + List<T> 조합 생성.Program.cs — Main 루틴XmlReaderSettings settings = new XmlReaderSettings()
{
IgnoreComments = true,
IgnoreWhitespace = true
};
using (XmlReader r = XmlReader.Create("PDL.xml", settings))
{
r.MoveToContent();
while(r.Read())
{
if (r.Depth == 1 && r.NodeType == XmlNodeType.Element)
ParsePacket(r);
}
<packet>만 파싱ParsePacket() 함수로 각 패킷 정의 파싱을 위임ParsePacket(XmlReader r)public static void ParsePacket(XmlReader r)
{
if (r.NodeType == XmlNodeType.EndElement)
return;
if (r.Name.ToLower() != "packet")
{
Console.WriteLine("Invalid packet node");
return;
}
string packetName = r["name"];
if (string.IsNullOrEmpty(packetName))
{
Console.WriteLine("Packet without name");
return;
}
Tuple<string, string, string> t = ParseMembers(r);
genPackets += string.Format(PacketFormat.packetFormat, packetName, t.Item1, t.Item2, t.Item3);
}
ParseMembers() 호출packetFormat 템플릿에 따라 클래스 생성 코드를 구성genPackets에 누적ParseMembers(XmlReader r)public static Tuple<string, string, string> ParseMembers(XmlReader r)
{
string packetName = r["name"];
string memberCode = "";
string readCode = "";
string writeCode = "";
int depth = r.Depth + 1;
while(r.Read())
{
if (r.Depth != depth)
break;
string memberName = r["name"];
if (string.IsNullOrEmpty(memberName))
{
Console.WriteLine("Member without name");
return null;
}
string memberType = r.Name.ToLower();
switch(memberType)
{
case "bool":
case "byte":
case "short":
case "ushort":
case "int":
case "long":
case "float":
case "double":
memberCode += string.Format(PacketFormat.memberFormat, memberType, memberName);
readCode += string.Format(PacketFormat.readFormat, memberName, ToMemberType(memberType), memberType);
writeCode += string.Format(PacketFormat.writeFormat, memberName, memberType);
break;
case "string":
memberCode += string.Format(PacketFormat.memberFormat, memberType, memberName);
readCode += string.Format(PacketFormat.readStringFormat, memberName);
writeCode += string.Format(PacketFormat.writeStringFormat, memberName);
break;
case "list":
Tuple<string, string, string> t = ParseList(r);
memberCode += t.Item1;
readCode += t.Item2;
writeCode += t.Item3;
break;
default:
Console.WriteLine("Unsupported type: " + memberType);
break;
}
}
return new Tuple<string, string, string>(memberCode, readCode, writeCode);
}
depth를 기준으로 현재 패킷의 직접 자식 노드만 처리readFormat, writeFormat 등을 사용해 필드 코드 생성ParseList()를 재귀적으로 호출ToMemberType(string type)public static string ToMemberType(string type)
{
switch (type)
{
case "bool": return "ToBoolean";
case "short": return "ToInt16";
case "ushort": return "ToUInt16";
case "int": return "ToInt32";
case "long": return "ToInt64";
case "float": return "ToSingle";
case "double": return "ToDouble";
default: return "";
}
}
BitConverter에 사용할 메서드명으로 치환public static string packetFormat =
@"
class {0}
{{
{1} // 멤버 변수
public void Read(ArraySegment<byte> segment)
{{
...
{2} // 읽기 구현부
}}
public ArraySegment<byte> Write()
{{
...
{3} // 쓰기 구현부
}}
}}";
{0}: 클래스 이름{1}: 멤버 변수들{2}: Read 구현{3}: Write 구현public static string memberFormat = @"public {0} {1};";
public static string readFormat =
@"this.{0} = BitConverter.{1}(s.Slice(count, s.Length - count));
count += sizeof({2});";
public static string writeFormat =
@"success &= BitConverter.TryWriteBytes(s.Slice(count, s.Length - count), this.{0});
count += sizeof({1});";
public static string readStringFormat =
@"ushort {0}Len = BitConverter.ToUInt16(s.Slice(count, s.Length - count));
count += sizeof(ushort);
this.{0} = Encoding.Unicode.GetString(s.Slice(count, {0}Len));
count += {0}Len;";
public static string writeStringFormat =
@"ushort {0}Len = (ushort)Encoding.Unicode.GetBytes(this.{0}, 0, this.{0}.Length, segment.Array, segment.Offset + count + sizeof(ushort));
success &= BitConverter.TryWriteBytes(s.Slice(count, s.Length - count), {0}Len);
count += sizeof(ushort);
count += {0}Len;";
PDL.xml:
<packet name="PlayerInfoReq">
<long name="playerId"/>
<string name="name"/>
</packet>
자동 생성된 코드:
class PlayerInfoReq
{
public long playerId;
public string name;
public void Read(ArraySegment<byte> segment)
{
ushort count = 0;
ReadOnlySpan<byte> s = new Span<byte>(segment.Array, segment.Offset, segment.Count);
count += sizeof(ushort);
count += sizeof(ushort);
this.playerId = BitConverter.ToInt64(s.Slice(count, s.Length - count));
count += sizeof(long);
ushort nameLen = BitConverter.ToUInt16(s.Slice(count, s.Length - count));
count += sizeof(ushort);
this.name = Encoding.Unicode.GetString(s.Slice(count, nameLen));
count += nameLen;
}
public ArraySegment<byte> Write()
{
ArraySegment<byte> segment = SendBufferHelper.Open(4096);
ushort count = 0;
bool success = true;
Span<byte> s = new Span<byte>(segment.Array, segment.Offset, segment.Count);
count += sizeof(ushort);
success &= BitConverter.TryWriteBytes(s.Slice(count, s.Length - count), (ushort)PacketID.PlayerInfoReq);
count += sizeof(ushort);
success &= BitConverter.TryWriteBytes(s.Slice(count, s.Length - count), this.playerId);
count += sizeof(long);
ushort nameLen = (ushort)Encoding.Unicode.GetBytes(this.name, 0, this.name.Length, segment.Array, segment.Offset + count + sizeof(ushort));
success &= BitConverter.TryWriteBytes(s.Slice(count, s.Length - count), nameLen);
count += sizeof(ushort);
count += nameLen;
success &= BitConverter.TryWriteBytes(s, count);
if (!success)
return null;
return SendBufferHelper.Close(count);
}
}
우리가 사용하는 PDL.xml 파일은 하나의 패킷 데이터를 XML 형태로 정의한 문서입니다. 이 문서에서 <packet> 태그는 하나의 클래스에 대응되며, 그 내부의 <long>, <string>, <list>와 같은 태그는 해당 클래스의 멤버 변수로 자동 변환됩니다. 핵심은 이 XML 문서를 기반으로 우리가 쓸 패킷 클래스를 자동 생성한다는 데 있습니다.
자동화 과정의 흐름은 다음과 같습니다.
Main() 진입 → XML 열기<packet> 태그 찾기ParsePacket() 호출로 각 패킷 이름 추출ParseMembers()를 통해 내부 멤버 파싱packetFormat 포맷에 결합하여 클래스 코드 생성Main()XmlReader r = XmlReader.Create(pdlPath, settings);
while (r.Read())
{
if (r.Depth == 1 && r.NodeType == XmlNodeType.Element)
ParsePacket(r);
}
Depth == 1 → <packet>만 필터링Read() 반복 → 스트림 방식으로 읽기ParsePacket()string packetName = r["name"];
Tuple<string, string, string> t = ParseMembers(r);
genPackets += string.Format(PacketFormat.packetFormat, packetName, t.Item1, t.Item2, t.Item3);
packetEnums += string.Format(PacketFormat.packetEnumFormat, packetName, ++packetId);
ParseMembers() 호출Tuple<string, string, string>은 각각 멤버, Read, Write 코드 조각genPackets, packetEnums에 추가ParseMembers()이 함수는 패킷 내부의 <long>, <string>, <list> 등의 태그를 읽고, 템플릿을 조립하여 코드 조각을 반환합니다.
switch(memberType)
{
case "int":
case "long":
case "float":
memberCode += ...;
readCode += ...;
writeCode += ...;
break;
case "string":
readCode += stringFormat;
writeCode += stringFormat;
break;
case "list":
Tuple<string, string, string> t = ParseList(r);
...
break;
}
ParseList()를 통해 구조체 선언까지 함께 생성ParseList() — 리스트 구조 처리리스트는 자동으로 다음과 같은 형태로 변환됩니다.
public class Skill
{
public int id;
public short level;
public float duration;
public void Read(...) { ... }
public bool Write(...) { ... }
}
public List<Skill> skills = new List<Skill>();
중첩된 리스트도 ParseMembers()를 재귀 호출하여 자동으로 처리합니다.
this.playerId = BitConverter.ToInt64(s.Slice(count, s.Length - count));
count += sizeof(long);
success &= BitConverter.TryWriteBytes(s.Slice(count, s.Length - count), this.playerId);
count += sizeof(long);
ushort nameLen = BitConverter.ToUInt16(s.Slice(count, s.Length - count));
this.name = Encoding.Unicode.GetString(s.Slice(count, nameLen));
interface IPacket
{
ushort Protocol { get; }
void Read(ArraySegment<byte> segment);
ArraySegment<byte> Write();
}
자동 생성된 모든 패킷 클래스는 IPacket 인터페이스를 상속받습니다. 이로써 모든 패킷은 동일한 방식으로 처리할 수 있게 됩니다.
_onRecv.Add((ushort)PacketID.C_PlayerInfoReq, MakePacket<C_PlayerInfoReq>);
_handler.Add((ushort)PacketID.C_PlayerInfoReq, PacketHandler.C_PlayerInfoReqHandler);
C_, 서버 패킷은 S_로 시작하도록 구분ClientPacketManager.cs, ServerPacketManager.cs로 분리 저장PDL.xml만 정의하면PacketGenerator.exe 실행 → GenPackets.cs 생성GenPackets.bat → Server와 DummyClient로 자동 배포PacketManager 자동 생성 및 등록까지 완료PacketHandler.cs)만 수동으로 구현하면 끝각 프로젝트에는 다음과 같은 폴더 구조가 있다고 가정합니다:
DummyClient/
└── Packet/
└── GenPackets.cs
Server/
└── Packet/
└── GenPackets.cs
이제 자동 생성기로 만들어진 GenPackets.cs, ClientPacketManager.cs, ServerPacketManager.cs를 여기에 배포합니다.
GenPackets.bat:: 패킷 생성
START ../../PacketGenerator/bin/PacketGenerator.exe ../../PacketGenerator/PDL.xml
:: 자동 복사
XCOPY /Y GenPackets.cs "../../DummyClient/Packet"
XCOPY /Y GenPackets.cs "../../Server/Packet"
XCOPY /Y ClientPacketManager.cs "../../DummyClient/Packet"
XCOPY /Y ServerPacketManager.cs "../../Server/Packet"
한 번만 더블 클릭하면, 전체 파일이 생성되고 자동 복사됩니다.
DummyClient 쪽ClientPacketManager.cs가 포함되어 있어야 하며,static void Main(string[] args)
{
PacketManager.Instance.Register();
...
}
OnRecvPacket()에서 호출합니다:public override void OnRecvPacket(ArraySegment<byte> buffer)
{
PacketManager.Instance.OnRecvPacket(this, buffer);
}
Server 쪽ServerPacketManager.cs를 사용하며,Main()에서 Register() 호출:PacketManager.Instance.Register();
public override void OnRecvPacket(ArraySegment<byte> buffer)
{
PacketManager.Instance.OnRecvPacket(this, buffer);
}
자동 생성된 코드만으로는 실제 패킷 처리 동작을 수행하지 않기 때문에, 핸들러 함수는 수동으로 작성해야 합니다.
C_PlayerInfoReq 처리public static void C_PlayerInfoReqHandler(PacketSession session, IPacket packet)
{
C_PlayerInfoReq p = packet as C_PlayerInfoReq;
Console.WriteLine($"PlayerInfoReq: {p.playerId} {p.name}");
foreach (var skill in p.skills)
{
Console.WriteLine($"Skill({skill.id})({skill.level})({skill.duration})");
foreach (var attr in skill.attributes)
{
Console.WriteLine($" Attribute({attr.att})");
}
}
}
as 캐스팅 후 필드를 직접 사용C_PlayerInfoReq packet = new C_PlayerInfoReq()
{
playerId = 1001,
name = "ABCD"
};
var skill = new C_PlayerInfoReq.Skill()
{
id = 101,
level = 1,
duration = 3.0f
};
skill.attributes.Add(new C_PlayerInfoReq.Skill.Attribute() { att = 77 });
packet.skills.Add(skill);
Send(packet.Write());
자동으로 C_PlayerInfoReqHandler() 호출됨:
public override void OnRecvPacket(ArraySegment<byte> buffer)
{
PacketManager.Instance.OnRecvPacket(this, buffer);
}
MakePacket<T>() → Read() → Handler() 호출까지 자동 흐름 수행좋습니다! 방금 올려주신 핵심 요약을 기반으로 한 글자도 빠짐없이, 논리 흐름을 유지하며, 강의용/문서용으로 다시 설명하는 형식으로 완성된 핵심 파트를 정리해드릴게요.
PDL 기반 패킷 자동 생성 시스템
이 강의의 핵심은 XML(PDL.xml)로 정의된 패킷 구조를 바탕으로, C# 패킷 클래스 및 매니저 코드까지 자동으로 생성하는 완전 자동화된 시스템을 구축하는 것입니다. 반복적인 패킷 작성, 핸들링 코드 구현, 등록 코드 작성 등을 모두 자동화하여 생산성, 일관성, 유지보수성을 동시에 확보합니다.
| 단계 | 설명 |
|---|---|
| 1. XML 정의 | <packet>, <int>, <string>, <list> 등으로 구성된 PDL.xml로 패킷 구조 선언 |
| 2. XmlReader 파싱 | Depth, NodeType을 기준으로 유효한 노드만 걸러서 읽기 |
| 3. Template 치환 | memberFormat, readFormat, writeFormat 등의 고정 템플릿에 데이터를 치환 |
| 4. 코드 생성 | 최종적으로 GenPackets.cs, ClientPacketManager.cs, ServerPacketManager.cs 파일 생성 |
| 5. 자동 배포 | .bat 파일을 통해 생성된 파일을 클라이언트/서버 경로에 자동 복사 |
string.Format()으로 치환하는 구조로 설계됨PacketFormat.cs에 모든 템플릿이 관리되고, Program.cs에서는 이 템플릿을 조합하여 최종 코드를 생성BitConverter 및 Encoding.Unicode를 활용한 고정 로직으로 일관성 유지ParseMembers()로 다시 파싱하는 구조struct가 아닌 class로 생성하여 내부에 다시 List를 가질 수 있도록 함GenPackets.cs에는 패킷 클래스뿐 아니라 PacketID enum도 자동 포함fileFormat, packetEnumFormat, packetFormat으로 전체 조립 후 저장| 개선 전 (기존 방식) | 개선 후 (자동화 시스템) |
|---|---|
switch-case로 수동 패킷 분기 | Dictionary<ushort, Action> 기반 자동 분기 |
| 패킷 클래스 수동 작성 | XML 기반 자동 생성 |
| 매번 Read/Write 구현 | 템플릿 기반 자동화 |
| 수동 핸들러 바인딩 | Register() 함수에서 자동 등록 |
| 항목 | 효과 |
|---|---|
switch-case 제거 | 수백 개 패킷도 O(1) 딕셔너리 탐색으로 처리 |
| 자동 등록 | 새로운 패킷 추가 시 Register만 수정 (혹은 자동 생성) |
| 핸들러 분리 | PacketHandler.cs에서 로직만 집중 |
| 확장성 확보 | Server/Client 패킷 분리, 보안성 강화, 다른 서버 유형 연동 가능 |
PDL.xml → (PacketGenerator.exe) → GenPackets.cs, ClientPacketManager.cs, ServerPacketManager.cs.bat 파일로 각 프로젝트의 Packet 폴더로 복사PacketGenerator/
├── Program.cs
├── PacketFormat.cs
├── bin/
└── PacketGenerator.exe
DummyClient/
└── Packet/
├── GenPackets.cs
└── ClientPacketManager.cs
Server/
└── Packet/
├── GenPackets.cs
└── ServerPacketManager.cs
| 항목 | 설명 |
|---|---|
.csproj 설정 | OutputPath를 bin/으로 설정하여 출력 경로 단순화 |
.bat 실행 | START PacketGenerator.exe PDL.xml + XCOPY /Y 로 자동 복사 |
외부 경로 인식 | args[0]으로 XML 경로 전달 가능하도록 수정 |
파일 정렬 | \\t, Environment.NewLine 등으로 출력 포맷 정리하여 가독성 향상 |
C_, S_ 접두어로 구분Register() 시점에 서버는 C_로 시작하는 패킷만, 클라이언트는 S_만 등록.bat로 패킷 생성부터 배포까지 단일 클릭으로 자동화