C#

Packet Generator #3

monstro 2025. 1. 8. 12:12
728x90
반응형

이번 포스트에서는 Packet의 포맷이 아닌

Packet을 담고 있는 파일 그 자체의 포맷을 만들도록 하겠습니다.

추가적으로 byte 자료형에 대한 처리와 

자료구조 안에 자료구조가 있는 경우도 어떻게 처리되는지 확인해보도록 하겠습니다.

 

1) PacketFormat

    class PacketFormat
    {
        public static string fileFormat =
@"using System;
using System.Collections.Generic;
using System.Net;
using System.Text;
using ServerCore;

public enum PacketID
{{
    {0}
}}

{1}
";
        // {0} 패킷 이름
        // {1} 패킷 번호
        public static string packetEnumFormat = 
@"{0} = {1},";


        // {0} 패킷 이름
        // {1} 패킷 멤버
        // {2} 멤버 변수 Read
        // {3} 멤버 변수 Write
        public static string packetFormat =
@"
class {0}
{{
    {1}
    public void Read(ArraySegment<byte> segment)
    {{
        ushort count = 0;

        ReadOnlySpan<byte> span = new ReadOnlySpan<byte>(segment.Array, segment.Offset, segment.Count);

        count += sizeof(ushort);
        count += sizeof(ushort);
        {2}
    }}

    public ArraySegment<byte> Write()
    {{
        ArraySegment<byte> segment = SendBufferHelper.Open(4096);
        bool success = true;
        ushort offset = 0;

        Span<byte> span = new Span<byte>(segment.Array, segment.Offset, segment.Count);
            
        offset += sizeof(ushort);
        success &= BitConverter.TryWriteBytes(span.Slice(offset, span.Length - offset), (ushort)PacketID.{0});
        offset += sizeof(ushort);
        {3}
        success &= BitConverter.TryWriteBytes(span, offset);

        if (success == false)
            return null;

        return SendBufferHelper.Close(offset);
    }}
}}
";

        // {0} 변수의 자료형
        // {1} 변수의 이름
        public static string memberFormat = 
@"public {0} {1};";


        // {0} 리스트 이름 {대문자}
        // {1} 리스트 이름 {대문자}
        // {2} 멤버 변수들
        // {3} 멤버 변수 Read
        // {4} 멤버 변수 Write
        public static string memberListFormat =
@"public new List<{0}> _{1}List = new List<{0}>();

public struct {0}
{{
    {2}

    public void Read(ReadOnlySpan<byte> span, ref ushort count)
    {{
        {3}
    }}

    public bool Write(Span<byte> span, ref ushort offset)
    {{
        bool success = true;

        {4}

        return success;
    }}
}}";

        // {0} 변수 이름
        // {1} To~변수의 자료형
        // {2} 변수의 자료형
        public static string readFormat =
@"this.{0} = BitConverter.{1}(span.Slice(count, span.Length - count));
count += sizeof({2});";

        // {0} 변수 이름
        // {1} 변수 자료형
        public static string readByteFormat = 
@" this.{0} = segment.Array[segment.Offset + count];
count += sizeof({1});
";

        // {0} 변수 이름
        public static string readStringFormat =
@"ushort {0}Len = BitConverter.ToUInt16(span.Slice(count, span.Length - count));
count += sizeof(ushort);
this.{0} = Encoding.Unicode.GetString(span.Slice(count, {0}Len));
count += {0}Len;";

        // {0} 리스트 이름 - 대문자
        // {1} 리스트 이름 - 소문자
        public static string readListFormat =
@"_{1}List.Clear();
ushort {1}Len = BitConverter.ToUInt16(span.Slice(count, span.Length - count));
count += sizeof(ushort);

for (int i = 0; i < {1}Len; i++)
{{ 
    {0} {1} = new {0}();
    {1}.Read(span, ref count);
    _{1}List.Add({1});
}}";

        // {0} 변수 이름
        // {1} 변수의 자료형
        public static string writeFormat =
@"success &= BitConverter.TryWriteBytes(span.Slice(offset, span.Length - offset), {0});
offset += sizeof({1});";

        // {0} 변수 이름
        // {1} 변수 자료형
        public static string writeByteFormat =
@"segment.Array[segment.Offset + offset] = this.{0};
offset += sizeof({1});
";

        // {0} 변수 이름
        public static string writeStringFormat =
@"ushort {0}Len = (ushort)Encoding.Unicode.GetBytes(this.{0}, 0, this.{0}.Length, segment.Array, segment.Offset + offset + sizeof(ushort));
success &= BitConverter.TryWriteBytes(span.Slice(offset, span.Length - offset), {0}Len);
offset += sizeof(ushort);
offset += {0}Len;";

        // {0} 리스트 이름 - 대문자
        // {1} 리스트 이름 - 소문자
        public static string writeListFormat =
@"success &= BitConverter.TryWriteBytes(span.Slice(offset, span.Length - offset), (ushort)_{1}List.Count);
offset += sizeof(ushort);

foreach ({0} {1} in _{1}List)
    success &= {1}.Write(span, ref offset);
";
    }

 

위와 같이 byte 자료형에 대한 Read와 Write를 추가하였고

또 스크립트 파일 자체의 형식을 가져와 파일을 생성할 수 있도록 설정하였습니다.

 

2) PacketGenerator

class Program
{
    static string genPackets;
    static ushort packetID;
    static string packetEnums;

    static void Main(string[] args)
    {
        XmlReaderSettings settings = new XmlReaderSettings()
        {
            IgnoreComments = true,
            IgnoreWhitespace = true,
        };

        using (XmlReader reader = XmlReader.Create("PDL.xml", settings))
        { 
            reader.MoveToContent();

            while (reader.Read())
            {
                // 파싱 중인 XML 파일의 깊이가 1이라는 것은 해당 XML 파일에 문제없이 접근한 것을 의미
                if (reader.Depth == 1)
                    ParsePacket(reader);
                
                // Console.WriteLine(reader.Name + " " + reader["name"]);
            }

            string FileText = string.Format(PacketFormat.fileFormat, packetEnums, genPackets);
            File.WriteAllText("GenPackets.cs", FileText);
        }
    }

    // XML 파일이 패킷 구조를 정의한 XML인지 판단하는 함수
    static public void ParsePacket(XmlReader reader)
    {
        // XML 구조에서 처음 읽어들인 단계가 마지막 element라면 리턴
        if (reader.NodeType == XmlNodeType.EndElement)
            return;

        // XML 구조에서 처음 읽어들인 단계가 packet이 아니라면 리턴
        if (reader.Name.ToLower() != "packet")
        {
            Console.WriteLine("Invalid Packet Node");
            return;
        }
            
        string packetName = reader["name"];
        
        // 파싱해온 XML 단계의 이름이 null이거나 비어있다면 리턴
        if (string.IsNullOrEmpty(packetName))
        {
            Console.WriteLine("Packet Without Name");
            return;
        }

        Tuple<string, string, string> packetTuple = ParsePacketMember(reader);
        genPackets += string.Format(
            PacketFormat.packetFormat,
            packetName,
            packetTuple.Item1,
            packetTuple.Item2,
            packetTuple.Item3
        );

        packetEnums += string.Format(
            PacketFormat.packetEnumFormat,
            packetName,
            ++packetID
        ) + Environment.NewLine + "\t";
    }

    // {1} 패킷 멤버
    // {2} 멤버 변수 Read
    // {3} 멤버 변수 Write
    // 패킷 구조를 정의한 XML 파일의 내부(=패킷의 멤버)를 파싱하는 함수
    static public Tuple<string, string, string> ParsePacketMember(XmlReader reader)
    {
        string packetName = reader["name"];

        string memberCode = "";
        string readCode = "";
        string writeCode = "";

        // XML의 구조에서 멤버를 순회하는 과정
        int depth = reader.Depth + 1;
        while (reader.Read())
        {
            if (reader.Depth != depth)
                break;

            string memberName = reader["name"];
            if (string.IsNullOrEmpty(memberName))
            {
                Console.WriteLine("Member Without Name");
                return null;
            }

            // 줄바꿈이 있는 경우, 개행하기 위한 코드
            if (string.IsNullOrEmpty(memberCode) == false)
                memberCode += Environment.NewLine;

            if (string.IsNullOrEmpty(readCode) == false)
                memberCode += Environment.NewLine;

            if (string.IsNullOrEmpty(writeCode) == false)
                memberCode += Environment.NewLine;

            // 파싱한 XML의 각 단계마다 동작을 수행
            string memberType = reader.Name.ToLower();
            switch (memberType)
            {
                case "byte":
                case "sbyte":
                    memberCode += string.Format(PacketFormat.memberFormat, memberType, memberName);
                    readCode += string.Format(PacketFormat.readByteFormat, memberName, memberType);
                    writeCode += string.Format(PacketFormat.writeByteFormat, memberName, memberType);
                    break;
                case "bool":
                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, ToMemeberType(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> tuple = ParseList(reader);
                    memberCode += tuple.Item1;
                    readCode += tuple.Item2;
                    writeCode += tuple.Item3;
                    break;
                default:
                    break;
            }
        }

        memberCode = memberCode.Replace("\n", "\n\t");
        readCode = readCode.Replace("\n", "\t\t");
        writeCode = writeCode.Replace("\n", "\t\t");

        return new Tuple<string, string, string>(memberCode, readCode, writeCode);
    }

    // 구조체를 저장한 리스트를 패킷에서 파싱하는 함수
    public static Tuple<string, string, string> ParseList(XmlReader reader)
    {
        string listName = reader["name"];
        if (string.IsNullOrEmpty(listName))
        {
            Console.WriteLine("List without Name");
            return null;
        }

        Tuple<string, string, string> tuple = ParsePacketMember(reader);

        string memberCode = string.Format(
            PacketFormat.memberListFormat,
            FirstCharToUpper(listName),
            FirstCharToLower(listName),
            tuple.Item1,
            tuple.Item2,
            tuple.Item3
        );

        string readCode = string.Format(
            PacketFormat.readListFormat,
            FirstCharToUpper(listName),
            FirstCharToLower(listName)
        );

        string writeCode = string.Format(
            PacketFormat.writeListFormat,
            FirstCharToUpper(listName),
            FirstCharToLower(listName)
        );

        return new Tuple<string, string, string>(memberCode, readCode, writeCode);
    }

    // 패킷에서 추출한 문자열로 표시된 자료형을 기준으로 To~ 함수를 찾는 함수
    public static string ToMemeberType(string memberType)
    {
        switch (memberType)
        {
            case "bool":
                return "ToBoolean";
            case "byte":
            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 "";
        }
    }

    // 문자열의 첫 시작 부분을 대문자로 바꾸는 함수
    public static string FirstCharToUpper(string input)
    { 
        if(string.IsNullOrEmpty(input))
            return "";
        return input[0].ToString().ToUpper() + input.Substring(1);
    }

    // 문자열의 첫 시작 부분을 소문자로 바꾸는 함수
    public static string FirstCharToLower(string input)
    {
        if (string.IsNullOrEmpty(input))
            return "";
        return input[0].ToString().ToLower() + input.Substring(1);
    }
}

 

지역변수를 추가하여 Packet의 ID와 Packet의 종류별 ID를 지정한 Enum을 저장하도록 하겠습니다.

또 자료형에서 byte의 경우 다른 방식으로 처리하도록 설정하였습니다.

 

3) 패킷 정의 XML

<PDL>
	<packet name ="PlayerInfoRequire">
		<long name = "_playerID"/>
		<byte name = "_nameByte"/>
		<string name = "_name"/>
		<list name = "Stat">
			<int name = "_statID"/>
			<short name = "_level"/>
			<float name = "_connectDuration"/>
			<list name = "Attribute">
				<int name="att"/>
			</list>
		</list>
	</packet>
	<packet name ="Test">
		<int name ="_TestNum"/>
	</packet>
</PDL>

 

XML 파일을 위와 같이 수정한 후 패킷을 생성하였습니다.

 

4) ServerSession

public override void OnConnected(EndPoint endPoint)
{
    Console.WriteLine($"[Connect from] : {endPoint}");

    PlayerInfoRequire packet = new PlayerInfoRequire(){ _playerID = 1001,_name = "Jake"};
    var Stat = new PlayerInfoRequire.Stat() { _statID = 1001, _level = 10, _connectDuration = 100.0f };
    Stat._attributeList.Add(new PlayerInfoRequire.Stat.Attribute() { att = 123 });
    packet._statList.Add(new PlayerInfoRequire.Stat() { _statID = 1001, _level = 10, _connectDuration = 100.0f });

    {
        ArraySegment<byte> segment = packet.Write();
        if (segment != null)
            Send(segment);
    }
}

 

이전과는 다르게 OnConnected에서 전달하는 패킷의 데이터를 일부 수정하였습니다.

기존의 세션들에서 사용하던 코드를 생성한 패킷으로 대체한 결과는 다음과 같습니다.

 

문제없이 동작하는 것을 확인할 수 있습니다.

728x90
반응형

'C#' 카테고리의 다른 글

Packet Generator #5  (0) 2025.01.20
Packet Generator #4  (0) 2025.01.08
Packet Generator #2  (0) 2025.01.07
Packet Generator #1  (0) 2025.01.07
직렬화(Serialization) 4  (0) 2025.01.06