C#/네트워크

[c#][서버] Packet Generator - 코드 작성 자동화

goliot 2024. 6. 9. 23:34
반응형

자동화

  • 지금까지 string, list 등 자료형의 처리에 대한 함수를 짜두었다.
  • 이를 자동화 하도록 하나로 합쳐보는 작업을 한다
  • 무언가 멤버가 하나 더 생길때마다 클래스에 찾아가서 한줄씩 추가하는 작업을 없애보자

Packet 정의

  • Packet이 어떻게 이뤄져 있는지 정의가 필요하다.
    • Xml로 해보자
/*class PlayerInfoReq
{
    public long playerId;
    public string name;

    public struct SkillInfo
    {
        public int id;
        public short level;
        public float duration;
    }
    public List<SkillInfo> skills = new List<SkillInfo>();
} //이거를 아래처럼 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>

Xml 파싱

  • 새로운 namespace를 추가하여 패킷을 파싱해본다
namespace PacketGenerator
{
    class Program
    {
        static void Main(string[] args)
        {
            XmlReaderSettings settings = new XmlReaderSettings()
            {
                IgnoreComments = true,
                IgnoreWhitespace = true,
            };

            //using 범위에서 벗어나면 r.Dispose()하여 닫는 작업을 자동으로 해줌
            using (XmlReader r = XmlReader.Create("PDL.xml", settings))
            {
                r.MoveToContent(); //헤더 건너뛰고 바로 내용으로

                while(r.Read())
                {
                    // 한칸 안으로 들어갔고, <packet ~ 처럼 시작하는 부분일 경우
                    if (r.Depth == 1 && r.NodeType == XmlNodeType.Element)
                        ParsePacket(r);
                    //Console.WriteLine(r.Name + " " + r["name"]);
                }
            }
        }

        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"]; //name="~" -> ~ 부분
            if(string.IsNullOrEmpty(packetName))
            {
                Console.WriteLine("Packet without name");
                return;
            }
            //여기까지 왔으면 packet안에 정상적으로 들어 온것

            ParseMembers(r);
        }

        public static void ParseMembers(XmlReader r)
        {
            string packetName = r["name"];

            int depth = r.Depth + 1; //1 + 1 = 2 -> 패킷의 내부
            while(r.Read())
            {
                if (r.Depth != depth) //패킷 내부 줄에서 벗어나면 종료
                    break;

                string memberName = r["name"];
                if(string.IsNullOrEmpty(memberName))
                {
                    Console.WriteLine("Member without name");
                    return;
                }

                string memeberType = r.Name.ToLower();
                switch (memeberType)
                {
                    case "bool":
                    case "byte":
                    case "short":
                    case "ushort":
                    case "int":
                    case "long":
                    case "float":
                    case "double":
                    case "string":
                    case "list":
                        break;
                    default:
                        break;
                } //추후 구현
            }
        }
    }
}
  • XmlReader.Depth 를 사용했다
    • Depth 1당, 탭으로 한칸 안으로 들어간다 라고 생각하면 편하다.

코드 작성 자동화

  • 우선 이전에 패킷을 만들던 부분에서, 핵심 부분만 따와서 Format을 만든다
namespace PacketGenerator
{
    class PacketFormat
    {
        // {0} 패킷 이름
        // {1} 패킷의 멤버 변수
        // {2} 멤버 변수 Read = readFormat, readStringFormat
        // {3} 멤버 변수 Write = writeFormat, writeStringFormat
        public static string packetFormat =
@"class {0}
{{
    {1}

    public void Read(ArraySegment<byte> segment)
    {{
        ushort count = 0;

        ReadOnlySpan<byte> s = 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); //최조 덩어리 생성

        ushort count = 0;
        bool success = true;

        Span<byte> s = new Span<byte>(segment.Array, segment.Offset, segment.Count);

        //단 한 비트라도 실패했는지 검사
        count += sizeof(ushort); //packet size
        success &= BitConverter.TryWriteBytes(s.Slice(count, s.Length - count), (ushort)PacketID.{0});
        count += sizeof(ushort); //packet id
        {3}
        success &= BitConverter.TryWriteBytes(s, count); //최종 크기
        if (success == false)
            return null;

        return SendBufferHelper.Close(count);
    }}
}}";
        // {0} 변수 형식
        // {1} 변수 이름
        public static string memberFormat =
@"public {0} {1};";

        // {0} 리스트 이름 [대문자]
        // {1} 리스트 이름 [소문자]
        // {2} 멤버 변수들
        // {3} 멤버 변수 Read
        // {4} 멤버 변수 Write
        public static string memberListFormat =
@"public class {0}
{{
	{2}

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

	public bool Write(Span<byte> s, ref ushort count)
	{{
		bool success = true;
		{4}
		return success;
	}}	
}}
public List<{0}> {1}s = new List<{0}>();";

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

        // {0} 변수 이름
        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 += nameLen;";

        // {0} 리스트 이름 [대문자]
        // {1} 리스트 이름 [소문자]
        public static string readListFormat =
@"this.{1}s.Clear();
ushort {1}Len = BitConverter.ToUInt16(s.Slice(count, s.Length - count));
count += sizeof(ushort);
for (int i = 0; i < {1}Len; i++)
{{
	{0} {1} = new {0}();
	{1}.Read(s, ref count);
	{1}s.Add({1});
}}";
        // {0} 변수 이름
        // {1} 변수 형식
        public static string writeFormat =
@"success &= BitConverter.TryWriteBytes(s.Slice(count, s.Length - count), this.{0});
count += sizeof({1});";

        // {0} 변수 이름
        public static string writeStringFormat =
@"ushort {0}Len = (ushort)Encoding.Unicode.GetBytes(this.{0}, 0, {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;";

        // {0} 리스트 이름 [대문자]
        // {1} 리스트 이름 [소문자]
        public static string writeListFormat =
@"success &= BitConverter.TryWriteBytes(s.Slice(count, s.Length - count), (ushort)this.{1}s.Count);
count += sizeof(ushort);
foreach ({0} {1} in this.{1}s)
	success &= {1}.Write(s, ref count);";
    }
}
  • {숫자} 안쪽을 채우는 것을 자동으로 돌아가게 하는 것이다!
    • 실제 중괄호를 넣고 싶으면 {{, }} 처럼 두번 써야 포맷이 아니라 그냥 괄호라고 인식한다.
static string genPackets; //새로 생길 cs파일의 소스코드가 string으로 저장됨

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

    //using 범위에서 벗어나면 r.Dispose()하여 닫는 작업을 자동으로 해줌
    using (XmlReader r = XmlReader.Create("PDL.xml", settings))
    {
        r.MoveToContent(); //헤더 건너뛰고 바로 내용으로

        while(r.Read())
        {
            // 한칸 안으로 들어갔고, <packet ~ 처럼 시작하는 부분일 경우
            if (r.Depth == 1 && r.NodeType == XmlNodeType.Element)
                ParsePacket(r);
            //Console.WriteLine(r.Name + " " + r["name"]);
        }

        File.WriteAllText("GenPackets.cs", genPackets); //생성된 string을 소스코드로 하는 cs 파일 생성
    }
}

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"]; //name="~" -> ~ 부분
    if(string.IsNullOrEmpty(packetName))
    {
        Console.WriteLine("Packet without name");
        return;
    }
    //여기까지 왔으면 packet안에 정상적으로 들어 온것

    Tuple<string, string, string> t = ParseMembers(r);
    genPackets += string.Format(PacketFormat.packetFormat, packetName, t.Item1, t.Item2, t.Item3);
}

// {1} 패킷의 멤버 변수
// {2} 멤버 변수 Read
// {3} 멤버 변수 Write
public static Tuple<string, string, string> ParseMembers(XmlReader r)
{
    string packetName = r["name"];

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

    int depth = r.Depth + 1; //1 + 1 = 2 -> 패킷의 내부
    while(r.Read())
    {
        if (r.Depth != depth) //패킷 내부 줄에서 벗어나면 종료
            break;

        string memberName = r["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)
            readCode += Environment.NewLine;
        if (string.IsNullOrEmpty(writeCode) == false)
            writeCode += Environment.NewLine;

        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":
                break;
            default:
                break;
        }
    }

    memberCode = memberCode.Replace("\n", "\n\t"); //들여쓰기 맞추기
    readCode = readCode.Replace("\n", "\n\t\t");
    writeCode = writeCode.Replace("\n", "\n\t\t");
    return new Tuple<string, string, string>(memberCode, readCode, writeCode);
}

public static string ToMemberType(string memberType)
{
    switch(memberType)
    {
        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 "";
    }
}
  • 이렇게 만들고 실행하면, .exe 파일이 만들어지는 경로에 다음과 같이 파일이 생긴다!

//GenPackets.cs
class PlayerInfoReq
{
    public long playerId;
	public string name;
	public class Skill
	{
		public int id;
		public short level;
		public float duration;
	
		public void Read(ReadOnlySpan<byte> s, ref ushort count)
		{
			this.id = BitConverter.ToInt32(s.Slice(count, s.Length - count));
			count += sizeof(int);
			this.level = BitConverter.ToInt16(s.Slice(count, s.Length - count));
			count += sizeof(short);
			this.duration = BitConverter.ToSingle(s.Slice(count, s.Length - count));
			count += sizeof(float);
		}
	
		public bool Write(Span<byte> s, ref ushort count)
		{
			bool success = true;
			success &= BitConverter.TryWriteBytes(s.Slice(count, s.Length - count), this.id);
			count += sizeof(int);
			success &= BitConverter.TryWriteBytes(s.Slice(count, s.Length - count), this.level);
			count += sizeof(short);
			success &= BitConverter.TryWriteBytes(s.Slice(count, s.Length - count), this.duration);
			count += sizeof(float);
			return success;
		}	
	}
	public List<Skill> skills = new List<Skill>();

    public void Read(ArraySegment<byte> segment)
    {
        ushort count = 0;

        ReadOnlySpan<byte> s = new ReadOnlySpan<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;
		this.skills.Clear();
		ushort skillLen = BitConverter.ToUInt16(s.Slice(count, s.Length - count));
		count += sizeof(ushort);
		for (int i = 0; i < skillLen; i++)
		{
			Skill skill = new Skill();
			skill.Read(s, ref count);
			skills.Add(skill);
		}
    }

    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); //packet size
        success &= BitConverter.TryWriteBytes(s.Slice(count, s.Length - count), (ushort)PacketID.PlayerInfoReq);
        count += sizeof(ushort); //packet id
        success &= BitConverter.TryWriteBytes(s.Slice(count, s.Length - count), this.playerId);
		count += sizeof(long);
		ushort nameLen = (ushort)Encoding.Unicode.GetBytes(this.name, 0, 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.Slice(count, s.Length - count), (ushort)this.skills.Count);
		count += sizeof(ushort);
		foreach (Skill skill in this.skills)
			success &= skill.Write(s, ref count);
        success &= BitConverter.TryWriteBytes(s, count); //최종 크기
        if (success == false)
            return null;

        return SendBufferHelper.Close(count);
    }
}
  • 만든 Format대로 꺽쇠 안이 채워져, 반복작업을 자동화할 수 있게 된다
  • 이렇게 만들어진 GenPackets.cs 의 코드들을 이전에 만들었던 서버, 클라이언트 세션에 복붙하면?

정상 작동한다!

 

반응형