수업


✅ 주제

PacketGenerator 자동화 시스템 구축 과정의 핵심

주제는 XML 기반 패킷 정의 파일(PDL.xml)을 이용해, 서버와 클라이언트가 공유하는 C# 패킷 코드를 자동으로 생성하는 전체 흐름과 구조를 구현하는 것입니다. 실무에 적용 가능한 완전 자동화된 패킷 생성기(PacketGenerator)를 만드는 것이 목표이며, 코드 작성 효율을 극대화하고 유지보수를 단순화하는 데 그 핵심이 있습니다.

1. XML 패킷 정의 기반 자동 코드 생성

  • PDL.xml 파일에 정의된 패킷 구조를 바탕으로, C#에서 사용할 수 있는 클래스 코드로 자동 변환합니다.
  • 이를 위해 XmlReader를 사용하여 XML을 파싱하고, 파싱된 결과를 PacketFormat.cs의 템플릿 문자열에 치환해 최종 코드로 출력합니다.
  • 패킷 클래스 내부의 멤버 선언, Read/Write 메서드 구현까지 자동화됩니다.

2. 템플릿 기반 코드 출력 자동화

  • 멤버 변수, List 구조체, 문자열(string), byte/sbyte 등 다양한 타입을 처리할 수 있는 템플릿을 미리 정의하고, 이를 통해 다양한 구조의 패킷 코드를 일관되게 생성합니다.
  • 포맷 정렬, 중첩 List 등 복잡한 패킷 구조도 템플릿으로 지원되며, 이 과정은 PacketFormat.cs 내부 문자열 조합으로 완성됩니다.

3. 자동 생성된 코드를 Server와 Client에 자동 배포

  • .bat 배치 파일을 통해, GenPackets.cs 및 이후 생성되는 PacketManager.cs 코드를 Server와 DummyClient 프로젝트에 자동 복사합니다.
  • 별도의 수동 이동 없이, 한 번의 실행으로 전체 프로젝트에 최신 패킷 코드가 반영되도록 자동화합니다.

4. 수동 switch-case 구조 제거 → Dictionary 자동 처리 구조

  • 기존의 ClientSession.OnRecvPacket() 내부에서 수백 개의 switch-case로 분기 처리하던 구조를 제거하고, Dictionary<ushort, Action<>> 구조로 완전히 전환합니다.
  • 각 패킷은 IPacket 인터페이스를 구현하며, MakePacket<T>()를 통해 자동 디시리얼라이즈 후 적절한 핸들러로 연결됩니다.
  • 이 구조는 성능 향상과 함께, 패킷 수 증가에 따른 유지보수 부담 최소화, 등록 실수 방지, 코드 가독성 향상이라는 장점을 동시에 제공합니다.

5. PacketManager 코드 자동 생성 및 분리

  • PacketManager.Register() 함수의 코드조차도 자동 생성되며, PacketFormat.managerRegisterFormat을 통해 등록 코드를 자동 출력합니다.
  • 패킷 이름 접두사(C_, S_)에 따라 클라이언트 전용/서버 전용 패킷으로 자동 분리되며, ClientPacketManager.cs, ServerPacketManager.cs로 각각 다른 프로젝트에 배포됩니다.
  • 이로써 패킷 사용 목적 구분, 보안성 향상, 멀티 서버 환경 확장성 확보가 가능해집니다.

좋습니다! 지금까지 제공해주신 내용을 기반으로, PacketGenerator 강의 전체에서 다루는 개념을 강사 스타일로 설명하는 형식으로 완전히 통합하여 재작성해드리겠습니다. 형식은 요청하신 대로 “왜 필요한가 → 개념 원리 → 자동화 흐름 → 한계 및 확장성” 순서로 정리하였습니다.


📚 개념

패킷 자동 생성 시스템의 구조와 원리


1. 왜 패킷 자동화 도구가 필요한가?

패킷 시스템은 네트워크 기반 프로그램에서 필수적인 구성 요소입니다. 특히 MMORPG처럼 수많은 요청과 응답이 오가는 시스템에서는 패킷의 수가 수십, 수백 개에 달하게 됩니다.

이때, 매번 C#으로 PlayerInfoReq, LoginReq, Test 같은 클래스를 수작업으로 만들고, 이에 맞춰 Read, Write 함수를 작성하며, PacketID enum과 switch-case 핸들러까지 수동으로 추가해야 한다면 어떻게 될까요?

  • 시간 낭비
  • 코드 중복
  • 버그 발생 가능성 증가

이 문제를 해결하기 위해 XML 기반 정의 파일(PDL.xml) 하나로, 모든 코드를 자동 생성할 수 있는 PacketGenerator 도구를 제작합니다.


2. XML로 패킷을 정의하고 코드로 자동 생성한다는 개념

패킷을 정의할 때는 다음과 같은 XML 포맷을 사용합니다.

<packet name="PlayerInfoReq">
  <long name="playerId"/>
  <string name="name"/>
</packet>

이 구조는 다음과 같이 해석됩니다:

  • <packet>: 하나의 클래스를 정의
  • <long>, <string>: 해당 클래스의 멤버 변수

이 구조를 기반으로 우리는 다음을 자동 생성합니다:

  • 클래스 선언부
  • 멤버 변수
  • Read() 함수 (역직렬화)
  • Write() 함수 (직렬화)

3. XmlReader를 사용하는 이유

C#의 XmlReader는 DOM 방식보다 메모리 효율이 높고, 단방향 순차 읽기 방식으로 속도가 빠릅니다.

우리는 XmlReader의 다음 기능을 활용합니다:

  • MoveToContent(): 시작 태그로 이동
  • Read(): 다음 노드로 이동
  • Depth: 현재 노드 깊이 체크
  • NodeType: Element, EndElement 구분

이를 통해 <packet>과 그 하위 멤버들을 정밀하게 파싱할 수 있습니다.


4. 템플릿 기반 자동화의 구조와 필요성

코드 자동 생성을 위해 단순한 문자열 조합이 아닌, 형식을 유지한 템플릿 문자열을 사용합니다.

예시:

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# 클래스를 만들어냅니다.


5. 자동화 파이프라인 전체 흐름 요약

패킷 자동화는 다음과 같은 단계를 거칩니다:

단계설명
XML 정의<packet>로 구조 정의 (PDL.xml)
ParsePacket()XML을 파싱하여 패킷 이름/멤버를 읽음
ParseMembers()멤버 변수별로 Read/Write 코드 분기 생성
PacketFormat.cs템플릿 정의: 클래스, enum, List, Read/Write
Program.cs최종적으로 string 조합 → GenPackets.cs 저장

그리고 .bat 파일을 통해 Server와 DummyClient에 자동 복사까지 수행합니다.


6. 고급 자동화 처리 (byte, sbyte, 중첩 리스트)

기본 자료형 외에도 자동화 처리를 위해 별도의 템플릿이 필요합니다:

  • byte: 배열 직접 접근
  • sbyte: 타입 캐스팅 필요
  • List: 복합 데이터 파싱 및 Write 처리
  • 2중 리스트: ParseList() 재귀 호출로 자동 대응 가능

이는 PacketFormat.cs에 세부 포맷을 모두 정의한 후, 자동 치환됩니다.


7. 자동 디스패치 처리 구조 (switch-case 제거)

기존 방식:

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);

이 코드조차 자동 생성됩니다.


8. PacketHandler 자동화는 왜 안 했을까?

  • PacketHandler는 게임 로직에 맞춰 동작하는 사용자 정의 핸들러입니다.
  • 자동 생성 시, 잘못된 로직이 들어가거나 충돌 가능성이 있으므로 수동 구현을 원칙으로 합니다.
  • 유지보수성과 명확한 목적 구분을 위해 반드시 수동 작성합니다.

9. 서버와 클라이언트 패킷 분리 필요성

패킷은 항상 양방향으로 오가지 않습니다.

예:

  • C_PlayerInfoReq: 클라이언트 → 서버
  • S_Test: 서버 → 클라이언트

따라서 다음과 같이 분리합니다:

  • ClientPacketManager.csS_ 접두어만 등록
  • ServerPacketManager.csC_ 접두어만 등록

이를 통해 보안 강화, 유지보수 분리, 분산 서버 대응이 가능합니다.


10. 자동화 배치파일의 역할

.bat 파일의 핵심 기능:

  1. PacketGenerator.exe 실행
  2. GenPackets.cs, PacketManager.cs 생성
  3. 파일 자동 복사
XCOPY /Y GenPackets.cs "../../DummyClient/Packet"
XCOPY /Y ServerPacketManager.cs "../../Server/Packet"

이제 한 번의 실행으로 모든 작업이 완료됩니다.


🧾 용어 정리

PacketGenerator 자동화 시스템

📁 [1] XML 및 파싱 관련 용어

용어설명
XML데이터 구조를 계층적으로 표현할 수 있는 마크업 언어. 패킷 정의에 사용됨.
PDL.xmlPacket Definition List. 패킷 정보를 XML로 정의한 설정 파일. <packet> 단위로 구조를 기술함.
XmlReaderC#에서 제공하는 경량 스트림 기반 XML 파서. 노드 단위 탐색이 가능하며 성능이 우수함.
DepthXML 트리 구조에서 현재 노드의 깊이. 루트는 0부터 시작함.
packetXML 내 하나의 <packet> 노드. 하나의 패킷 단위 구조를 의미.
args[]프로그램 실행 시 외부에서 전달되는 인자값 배열. XML 경로 등을 지정할 때 사용함.

🧱 [2] 코드 자동 생성 템플릿 관련 용어

용어설명
PacketFormat.cs패킷 자동 생성 시 사용하는 코드 템플릿 정의 클래스. 문자열 포맷으로 정의됨.
packetFormat하나의 패킷 클래스 전체 정의를 구성하는 템플릿 문자열.
fileFormat최종 .cs 파일 전체 구조를 구성하는 템플릿 (PacketID enum + 클래스 목록 포함).
managerFormatPacketManager 클래스 전체를 자동 생성하기 위한 템플릿.
managerRegisterFormatRegister() 함수 내 각 패킷을 등록하는 구문 템플릿.

📌 [3] 멤버 변수 및 I/O 처리 템플릿 용어

용어설명
memberFormat단순 변수 선언을 위한 템플릿. (e.g., public int id;)
readFormat / writeFormat기본형 타입(int, float 등)의 Read/Write 코드 템플릿.
readStringFormat / writeStringFormat문자열(string) 전용 Read/Write 템플릿.
readByteFormat / writeByteFormatbyte/sbyte 등 특수 타입에 대한 직접 인덱싱 기반 Read/Write 템플릿.
memberListFormatList<T> 멤버를 위한 내부 구조체 선언 + 리스트 선언용 템플릿.
readListFormat / writeListFormat리스트 항목 반복 처리용 Read/Write 템플릿.

🔁 [4] 파싱 및 템플릿 치환 관련 함수

용어설명
ParsePacket()하나의 <packet> 노드를 파싱하여 클래스 코드 + 핸들러 등록 구문을 생성하는 함수.
ParseMembers()<packet> 내 멤버 변수들을 파싱하고, 멤버/Read/Write 템플릿에 맞춰 코드 조각 생성.
ParseList()<list> 노드를 파싱하여 중첩 리스트(2중 리스트 포함) 지원하는 구조를 생성하는 함수.
ToMemberType()BitConverter의 ToXxx() 메서드명을 반환하기 위한 타입 매핑 함수.
FirstCharToUpper() / FirstCharToLower()리스트 구조체 이름 생성 시 대소문자 조정용 유틸 함수.

🧰 [5] 자동 생성 및 출력 관련 용어

용어설명
genPackets자동 생성된 전체 클래스 코드 문자열을 누적 저장하는 전역 변수.
packetEnumsPacketID에 들어갈 enum 목록을 누적 저장하는 문자열 변수.
string.Format()포맷 문자열의 {0}, {1} 등에 실 데이터를 삽입하는 함수.
Tuple<string, string, string>(멤버 선언부, Read 구문, Write 구문) 세 가지 문자열을 하나로 묶는 자료형.

🚀 [6] 배치 및 파일 복사 자동화 용어

용어설명
GenPackets.csPacketGenerator에 의해 자동 생성되는 패킷 클래스 전체 코드 파일.
ClientPacketManager.cs / ServerPacketManager.csS / C 구분된 패킷만 포함하는 자동 생성된 PacketManager 파일.
GenPackets.batPacketGenerator를 실행하고 생성된 파일을 각 프로젝트에 자동 복사하는 배치 파일.
START외부 실행파일(예: PacketGenerator.exe)을 실행하는 명령어.
XCOPYWindows에서 파일을 복사하는 명령어. /Y 옵션을 추가하면 기존 파일을 덮어쓴다.

🎯 [7] Packet 처리 구조 관련 용어

용어설명
PacketID각 패킷에 고유하게 할당되는 enum 값. 프로토콜 구분 용도로 사용됨.
IPacket모든 패킷 클래스가 공통으로 구현해야 하는 인터페이스.
Register()PacketManager 내에서 각 패킷 ID에 대한 처리 함수와 핸들러를 등록하는 함수.
MakePacket<T>()제네릭을 사용해 패킷 인스턴스를 만들고, .Read() 수행 후 핸들러를 실행하는 함수.
Dictionary<ushort, Action<...>>패킷 ID에 따라 실행할 함수를 연결하는 매핑 테이블.

좋아, 여기까지 받은 코드 분석 파트 전부 문제없이 합쳐서 정리할 수 있어. 한 글자도 빠짐없이, 누락 없이, 그리고 흐름까지 자연스럽게 이어서 완성된 형태로 만들어줄게.

작성할 때는 다음 기준을 맞출게:

  • 블로그나 강의 자료로 바로 써도 될 수준의 설명
  • 순차적으로 코드 흐름이 이해되도록 구성
  • 실제 자동 생성기 흐름에 맞춘 구조적 구분
  • ✅ 코드/템플릿/예제/파일 흐름까지 전부 통합

✅ 코드 분석

XML 기반 패킷 클래스 자동 생성 시스템

📂 PDL 구조 이해

✨ 예시: 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> 조합 생성.

📂 XML 파싱 흐름

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에 사용할 메서드명으로 치환

📂 PacketFormat.cs 템플릿 구조

▶ 클래스 전체 템플릿

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 구현

▶ 멤버, Read, 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 구조와 클래스 매핑

우리가 사용하는 PDL.xml 파일은 하나의 패킷 데이터를 XML 형태로 정의한 문서입니다. 이 문서에서 <packet> 태그는 하나의 클래스에 대응되며, 그 내부의 <long>, <string>, <list>와 같은 태그는 해당 클래스의 멤버 변수로 자동 변환됩니다. 핵심은 이 XML 문서를 기반으로 우리가 쓸 패킷 클래스를 자동 생성한다는 데 있습니다.


🔎 주요 흐름

자동화 과정의 흐름은 다음과 같습니다.

  1. Main() 진입 → XML 열기
  2. <packet> 태그 찾기
  3. ParsePacket() 호출로 각 패킷 이름 추출
  4. ParseMembers()를 통해 내부 멤버 파싱
  5. 멤버 변수 선언 / 읽기(Read) / 쓰기(Write) 코드 템플릿 생성
  6. packetFormat 포맷에 결합하여 클래스 코드 생성
  7. 최종적으로 파일로 저장하거나 콘솔에 출력

📄 핵심 함수 구조

📌 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()를 재귀 호출하여 자동으로 처리합니다.


📄 템플릿 예시

📌 Read 템플릿

this.playerId = BitConverter.ToInt64(s.Slice(count, s.Length - count));
count += sizeof(long);

📌 Write 템플릿

success &= BitConverter.TryWriteBytes(s.Slice(count, s.Length - count), this.playerId);
count += sizeof(long);

📌 String Read/Write

ushort nameLen = BitConverter.ToUInt16(s.Slice(count, s.Length - count));
this.name = Encoding.Unicode.GetString(s.Slice(count, nameLen));

✅ IPacket 인터페이스 도입

interface IPacket
{
    ushort Protocol { get; }
    void Read(ArraySegment<byte> segment);
    ArraySegment<byte> Write();
}

자동 생성된 모든 패킷 클래스는 IPacket 인터페이스를 상속받습니다. 이로써 모든 패킷은 동일한 방식으로 처리할 수 있게 됩니다.


✅ PacketManager 자동 생성

_onRecv.Add((ushort)PacketID.C_PlayerInfoReq, MakePacket<C_PlayerInfoReq>);
_handler.Add((ushort)PacketID.C_PlayerInfoReq, PacketHandler.C_PlayerInfoReqHandler);
  • 클라이언트 패킷은 C_, 서버 패킷은 S_로 시작하도록 구분
  • 이 등록 코드도 자동 생성기로 템플릿화되어 생성됩니다
  • ClientPacketManager.cs, ServerPacketManager.cs로 분리 저장

✅ 전체 자동화 흐름

  1. PDL.xml만 정의하면
  2. PacketGenerator.exe 실행 → GenPackets.cs 생성
  3. GenPackets.bat → Server와 DummyClient로 자동 배포
  4. PacketManager 자동 생성 및 등록까지 완료
  5. 핸들러(PacketHandler.cs)만 수동으로 구현하면 끝

📦 연동 준비

1. DummyClient / Server 구조 준비

각 프로젝트에는 다음과 같은 폴더 구조가 있다고 가정합니다:

DummyClient/
└── Packet/
    └── GenPackets.cs

Server/
└── Packet/
    └── GenPackets.cs

이제 자동 생성기로 만들어진 GenPackets.cs, ClientPacketManager.cs, ServerPacketManager.cs를 여기에 배포합니다.


2. 자동 배포 스크립트 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"

한 번만 더블 클릭하면, 전체 파일이 생성되고 자동 복사됩니다.


🔌 프로젝트 연동

1. 클라이언트 DummyClient

  • ClientPacketManager.cs가 포함되어 있어야 하며,
  • 진입점(Main)에서 다음처럼 초기화합니다:
static void Main(string[] args)
{
    PacketManager.Instance.Register();
    ...
}
  • 클라이언트 수신은 OnRecvPacket()에서 호출합니다:
public override void OnRecvPacket(ArraySegment<byte> buffer)
{
    PacketManager.Instance.OnRecvPacket(this, buffer);
}

2. 서버 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);
}
  • 내부에서 Dictionary → 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에서는 이 템플릿을 조합하여 최종 코드를 생성
  • Read/Write도 자동으로 구현되며, BitConverterEncoding.Unicode를 활용한 고정 로직으로 일관성 유지
  • List, 구조체, 중첩 구조까지 지원하는 확장 가능한 시스템

🧠 중요한 구현 전략

  • List 자동화의 핵심은 내부 멤버를 재귀적으로 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 설정OutputPathbin/으로 설정하여 출력 경로 단순화
.bat 실행START PacketGenerator.exe PDL.xml + XCOPY /Y 로 자동 복사
외부 경로 인식args[0]으로 XML 경로 전달 가능하도록 수정
파일 정렬\\t, Environment.NewLine 등으로 출력 포맷 정리하여 가독성 향상

🔒 클라이언트/서버 패킷 완전 분리 전략

  • 패킷 명을 C_, S_ 접두어로 구분
  • Register() 시점에 서버는 C_로 시작하는 패킷만, 클라이언트는 S_만 등록
  • 보안성 강화 및 해킹 취약점 제거에 효과적

🎯 정리

  1. XML 정의만으로 전체 패킷 클래스가 자동 생성됨
  2. 템플릿 시스템으로 확장성과 유지보수성이 우수함
  3. List, 구조체, 중첩까지 재귀적으로 자동 생성됨
  4. .bat로 패킷 생성부터 배포까지 단일 클릭으로 자동화
  5. PacketManager까지 자동 생성되어 수신 처리 코드도 switch 없이 동작
  6. 클라이언트/서버 패킷 구분을 통해 안정성 확보

profile
李家네_공부방

0개의 댓글