Server programming

03_08_패킷 직렬화_PacketGenerator#3_전체 코드 생성

devRiripong 2023. 4. 18.
반응형

이번 시간에 한 내용

  1. GetPacket.cs 파일 안의 전체 코드 생성을 위한 포멧을 정의 하고 그걸 이용해 코드를 생성한다.
  2. PDL에 패킷이 여러개로 정의 된 경우 테스트 한다.
  3. 생성된 코드가 정렬되게 수정한다.
  4. 타입이 byte인 경우 처리될 수 있게 코드 포멧을 추가하고 코드를 생성한다.
  5. 2중 List가 지원되는지 테스트 한다.

1. 파일 자체에 대한 포멧 정의 하기

지난 시간에 패킷 자동화에 대한 윤곽을 봤었는데

이제 앞으로는 GenPackets.cs에 있는 내용을 수동으로 복붙해서 붙여 넣는게 아니라,

GenPackets라는 파일 자체든 클라든 서버든 필요한 쪽에서 참조를 해가지고, 사용하게끔 수정을 해야 한다.

이 파일이 완전체로 사용되기 위해서 뭐가 빠졌는지 생각을 해보면, ServerSessions에서 사용하던걸 보면,

public enum PacketID
    {
        PlayerInfoReq = 1,
        PlayerInfoOk = 2,
    }

이런 식으로 enum 값이 있어야 하고,

using System;
using System.Collections.Generic;
using System.Text;
using System.Net;
using System.Net.Sockets;
using System.Threading;
using ServerCore;

이런 using 코드가 있어야 에러가 안나고 이 파일을 사용할 수 있을 거야.

결국 PacketFormat에 돌아가서 몇 개만 더 추가를 해주도록 하자.

 

아래는 Packet에 대한 포멧이었고 이제는 완전체인 File 자체에 관한 포멧을 정의해주도록 하자.

PacketFormat.cs에서

public static string fileFormat =
@"

";

일단은 ServerSession에서

using System.Net.Sockets;
using System.Threading;

얘네 둘은 사용하지 않고 있고,

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

public enum PacketID
{{
    PlayerInfoReq = 1,
    PlayerInfoOk = 2,
}}
";

사용하는 것만 이렇게 복붙을 해준다.

그리고 PacketID목록을 1,2번 상태로 갖고 있어야 한다.

복붙을 하고 소괄호를 2개로 맞춰준다.

그리고 자동화 할 것을 구분한다.

PacketID 부분은 **{0}**으로 집어 준다.

이어서 아래에 class PlayerInfoReq의 패킷 목록들이 아래 쭉 들어가게 될거야. 그걸 합한게 GenPackets.cs에 완성된 파일 내용이 될 테니까, 그 내용을 {1}로 집어 준다.

				// {0} 패킷 이름/번호 목록
        // {1} 패킷 목록
public static string fileFormat =
@"
using System;
using System.Collections.Generic;
using System.Text;
using System.Net; 
using ServerCore;

public enum PacketID
{{
    {0}
}}

{1}
";

그 다음에 또 한칸을 내려가지고, 각각 내용을 채워 보도록 하자.

**{1}**같은 경우에는 우리가 packetFormat을 하나하나씩 다 긁어서 붙여주면 될테고,

**{0}**같은 경우에는 enum을 해서 시리즈를 맞춰줘야 되니

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

이렇게 파주도록 하자.

public enum PacketID만 다 완성해가지고, fileFormat에다가 {0}과 {1}을 각각 조립을 해서 넣어주면, 완성된 GenPackets.cs 라는 파일이 튀어나오게 된다.

2. 정의한 포멧대로 코드 생성 하기

여기까지 했으면 PacketGenerator의 Program.cs 쪽으로 가서, 우리가 방금 추가한 부분을 완성을 시켜 보도록 하자.

Main에서

File.WriteAllText("GenPackets.cs", genPackets);

GenPackets.cs라는 파일에다가 계속 합쳐 놨던 genPackets라는 string을 통째로 넣어주고 있었는데, 이제는 얘를 넣어주면 되는게 아니라, 한번 더 단계를 거쳐야 한다.

string.Format(PacketFormat.fileFormat, "", "");

이렇게 fileFormat이란 코드를 만들어 주는데 {0},{1}을 각각 “”에 넣어주면 된다.

				// {0} 패킷 이름/번호 목록
        // {1} 패킷 목록
public static string fileFormat =

{1}같은 경우는 static string genPacket으로 만들어 주고 있었던 애를 그대로 넣어주면 되니까, 두번째 “”에 들어가면 될거고,

string.Format(PacketFormat.fileFormat, "", genPackets);

그 다음에 PacketFormat에서 첫번째 인자로 넘겨준 애가, 새로 만들어 줘야 되는 패킷 이름/번호 목록이 된다.

기존에 하던 것과 마찬가지로,

static string packetEnums;

string으로 만들어 주자.

얘를 만들어 주기 위해서는 패킷을 몇개를 처리 했는지를 기억 해야지만 순서대로 1,2,3,4,5번을 매길 수 있을거야.

static ushort packetId;

이 변수를 둬가지고, 0번부터 해서 하나씩 쭉 늘려 주도록 할거야.

namespace 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 r = XmlReader.Create("PDL.xml", settings))
            {
                r.MoveToContent(); 

                while(r.Read())
                {
                    if (r.Depth == 1 && r.NodeType == XmlNodeType.Element)
                        ParsePacket(r); 
                    //Console.WriteLine(r.Name + " " + r["name"]); 
                }

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

결국 여기 ParsePacket(r); 이 다 완성이 되어가지고 최종 결과물이 나올 떄 즈음에는

string.Format(PacketFormat.fileFormat, packetEnums, genPackets);

packetEnums를 넣어 줘가지고, string을 만들어 줘야 한다.

그럼

string fileText = string.Format(PacketFormat.fileFormat, packetEnums, genPackets);

fileText가 string 형식으로 튀어 나오게 될거고,

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

이런 식으로최종 결과물로 뽑아오면 된다.

 

이제 방금 넣어준 packetEnums를 만들어줄 차례가 왔다.

genPackets를 어디서 하고 있는지 살펴 보면, ParsePacket이라는 애 안에서,

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

이게 하나의 패킷 단위니까, 끝에서 genPackets에 추가를 하고 있었어. 그렇다는 건 마찬가지로 Packet하나를 파싱할 때 마다

genPackets += string.Format(PacketFormat.packetFormat, packetName, t.Item1, t.Item2, t.Item3);
packetEnums += string.Format(PacketFormat.packetEnumFormat, packetName, ++packetId); 
}

이렇게 넣어주면 되는데 packetEnumFormat의 인자 패킷 번호는 정책에 따라 달라질 수 있다.

지금같은 경우는 0번부터 1,2,3,4,5 이렇게 쭉쭉 해주면 되니까, packetId에 하나씩 늘리면서 넣어주도록 하자.

여기까지 하면 어떤식으로 결과물이 나오는지 살펴보자.

3. 중간 테스트 하기

PacketGenerator를 시작 프로젝트로 설정해주고 실행을 하면

GenPackets.cs가

using System;
using System.Collections.Generic;
using System.Text;
using System.Net; 
using ServerCore;

public enum PacketID
{
    PlayerInfoReq = 1
}

class PlayerInfoReq 
{
  	public long playerId;
	public string name;
	
	public struct 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 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;
        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);

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

이런식으로 생성된다.

4. 패킷이 여러개면 어떻게 될지 실험하기

만약 여러개가 있으면 잘 실행이 되는지 궁금하니까,

public enum PacketID
{
    PlayerInfoReq = 1
}

PDL에 패킷 하나만 더 추가해보자.

<?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>
  <packet name ="Test">
    <int name ="testInt"/>
  </packet>
</PDL>

저장한 다음에 PDL의 경로를 bin/Debug/netcoreapp3.1 여기 실행파일이 있는 에다 넣어준다.

여기서 PacketGenerator.exe을 실행해보자.

GenPackets에 가서 보면

class Test 
{
    public int testInt;     

    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.testInt = BitConverter.ToInt32(s.Slice(count, s.Length - count));
				count += sizeof(int);
    }

    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.Test);
        count += sizeof(ushort);
        success &= BitConverter.TryWriteBytes(s.Slice(count, s.Length - count), this.testInt);
				count += sizeof(int);
        success &= BitConverter.TryWriteBytes(s, count);
        if (success == false)
            return null;
        return SendBufferHelper.Close(count);
    }
}

이렇게 수정이 되었는데

Test는 잘 만들어 졌는데

5. 정렬하기

public enum PacketID
{
    PlayerInfoReq = 1,Test = 2
}

이부분이 좀 불만이야.

엔터가 눌러진 상태로 되어야 정상일거야.

Program.cs의 packetEnums로 가서,

packetEnums += string.Format(PacketFormat.packetEnumFormat, packetName, ++packetId) + Environment.NewLine;

줄 바꾸기 코드를 넣어준다.

실행해 보면

public enum PacketID
{
    PlayerInfoReq = 1,
Test = 2,

}

탭이 하나 빠져 있으니까,

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

이렇게 tab을 추가해준다.

public enum PacketID
{
    PlayerInfoReq = 1,
		Test = 2,
	
}

이제는 정렬이 됐다.

이걸 1씩 늘리거나 셔플해서 해킹하는 사람 헷갈리게 할지는 나중에 하도록 할지는 나중에 정의하게 되겠고, 그거에 따라서 로직을 수정해야 되겠지만 지금 단계에서는 간단하게 나열을 해서 만드는 것도 잘못된 건 아니니까 일단은 이렇게 한다.

그리고 마음에 안드는 부분 PacketFormat.cs에서 수정해 주면 된다. 한줄 띄어져 있거나 하는 부분. 같은 거.

6. 타입이 byte인 경우 포멧 추가

마지막에 지난 시간에 하려다가 처리 안한 부분이 있었는데 byte인 경우를 처리 안하고 갔었어.

Read, Write를 보면

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

이런 식으로 BitConverter를 이용해서 변환을 하고 있었는데

byte배열에서 byte를 꺼내는 BitConverter를 이용하는게 아니라, 그냥 index를 이용해서 바로 빼올 수 있으니까 애당초 인터페이스가 안맞았었어. 그래서 걔만 예외적으로 하나를 추가해주도록 할거야.

어떤 식으로 하는지 보기위해 가상의 시나리오를 만들어 보자.

GenPacket.cs에서

class PlayerInfoReq 
{
	public byte testByte; 
  public long playerId;
	public string name;

여기에 testByte라는 인자가 하나 더 있었다고 가정을 해보자.

PDL에 byte하나를 추가해주자.

<?xml version="1.0" encoding="utf-8" ?>
<PDL>
  <packet name ="PlayerInfoReq">
    <byte name ="testByte"/>
    <long name ="playerId"/>
    <string name ="name"/>
    <list name ="skill">
      <int name ="id"/>
      <short name ="level"/>
      <float name ="duration"/>
    </list>
  </packet>
  <packet name ="Test">
    <int name ="testInt"/>
  </packet>
</PDL>

지금은 실행해도 파싱이 안될거야. 코드를 안 넣었으니까. 수동으로 넣어준 다음에 어떤식으로 자동화 할지를 찾아보면 된다.

Read에서 어떤식으로 변환될지 생각해보자. 가상의 코드를 추가해 인터페이스를 만들어 보자.

BitConverter 안쓰고 그냥 Byte를 추출해 오면 된다.

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);
				// TODO
				this.testByte = segment.Array[segment.Offset + count];
				count += sizeof(byte); 
				//
        this.playerId = BitConverter.ToInt64(s.Slice(count, s.Length - count));
				count += sizeof(long);

//TODO 이 부분이 우리가 자동화 해야 하는 코드,

Write같은 경우에도 이 코드를 복사해서

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

				// TODO
				segment.Array[segment.Offset + count] = this.testByte; 			;
				count += sizeof(byte);
				//

이걸 자동화 하는 작업을 해볼거야.

PacketFormat.cs에 추가를 해야 하는데

일반적인 상황에서는 readFormat이란 애를 사용하고 있었는데

public static string readByteFormat =
@"this.testByte = segment.Array[segment.Offset + count];
count += sizeof(byte);";

이걸

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

이렇게 해주면 된다.

Write도 비슷할테니 이걸 복사해서 writeFormat 밑에 붙여넣어 준다.

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

7. 정의한 포멧대로 byte 처리 코드 생성

여기까지 했으면 다시 Program.cs로 가서 올려보자.

ParseMembers의 switch(memberType)에서 case “byte”를 추가해준다.

case "byte":
case "sbyte":
    memberCode += string.Format(PacketFormat.memberFormat, memberType, memberName);
    readCode += string.Format(PacketFormat.readFormat, memberName, ToMemberType(memberType), memberType);
    writeCode += string.Format(PacketFormat.writeFormat, memberName, memberType);
    break;

이렇게 해준다.

일단 memberFormat같은 경우 PacketFormat에서

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

이렇게 되어 있는데 이건 변하지 않을 거니까 그대로 나둬도 되지만, readFormat, writeFormat이 변하게 될거야.

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

이렇게 바꿔주면 된다.

8. byte 처리 코드 생성 테스트

이렇게 했으면 나머지 부분은 크게 수정할 게 없는 거 같아.

테스트를 위해 PDL.xml을 다시 실행파일 있는 곳으로 복붙을 해준다.

그리고 PacketGenerator를 실행해주고 GenPacket에 가보면

this.testByte = segment.Array[segment.Offset + count];
		count += sizeof(byte);
segment.Array[segment.Offset + count] = this.testByte;
		count += sizeof(byte);

이 코드가 늘어나 있는 것을 볼 수 있다.

이런 식으로 뭔가 하나를 만들어 놓은 상태에서 조금씩 자동화 코드를 수정하는 작업은 필요하게 될 거고, 크게 어려운 작업은 아니다 .

9. sbyte인 경우 챙기기

실험해 봤을 때 만약 byte가 아니라 sbyte로 실행하게 되면, byte배열에 바로 넣어 줄 수는 없고, cast를 한번 하긴 해야해, 그래서 걔까지 챙겨 주자면

PacketFormat.cs에서

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

캐스팅 하는 부분을 넣어서 안전장치만 마련해주면 나머지는 문제 없이 동작을 한다.

그래서 byte까지도 처리가 됐고, 이 정도면 훌륭하게 어느정도 준비가 된거.

10. 2중 List 지원

혹시라도 눈치 챘을 수 있는데 PDL에서 우리가

<?xml version="1.0" encoding="utf-8" ?>
<PDL>
  <packet name ="PlayerInfoReq">
    <byte name ="testByte"/>
    <long name ="playerId"/>
    <string name ="name"/>
    <list name ="skill">
      <int name ="id"/>
      <short name ="level"/>
      <float name ="duration"/>
    </list>
  </packet>
  <packet name ="Test">
    <int name ="testInt"/>
  </packet>
</PDL>

list안에다가 int, short, float를 넣고 있었어.

경우에 따라 list안에 list를 또 넣어서, 2중 list를 지원하는게 좋은 경우도 있다.

예를 들어

<?xml version="1.0" encoding="utf-8" ?>
<PDL>
  <packet name ="PlayerInfoReq">
    <byte name ="testByte"/>
    <long name ="playerId"/>
    <string name ="name"/>
    <list name ="skill">
      <int name ="id"/>
      <short name ="level"/>
      <float name ="duration"/>
      <list name ="attribute">
        <int name ="att"/>
      </list>
    </list>
  </packet>
  <packet name ="Test">
    <int name ="testInt"/>
  </packet>
</PDL>

이런 식으로 skill안에 속성 같은 느낌으로 list를 만들고 싶다고 가정을 해보자.

이것도 똑같은 방법으로 자동화 한다면 똑같이 구현 되게끔 할 수 있다

운좋은 지금 한 것에서 처리가 될 수 있는데 궁금하니 실행해 보자.

실행 파일에 있는 PDL파일을 열어서 위와 같이 skill안에 attribute list를 넣는 조작을 하고 실행을 해보자.

그리고 GenPackets.cs를 살펴 보면

public struct Skill
	{
	  public int id;
		public short level;
		public float duration;
		
		public struct Attribute
		{
		    public int att;
		
		    public void Read(ReadOnlySpan<byte> s, ref ushort count)
		    {
		        this.att = BitConverter.ToInt32(s.Slice(count, s.Length - count));
				count += sizeof(int);
		    }
		
		    public bool Write(Span<byte> s, ref ushort count)
		    {
		        bool success = true;
		        success &= BitConverter.TryWriteBytes(s.Slice(count, s.Length - count), this.att);
						count += sizeof(int);
		        return success;
		    }
		}
		public List<Attribute> attributes = new List<Attribute>();

Skill안에 Attribute가 생긴 것을 볼 수 있다.

그리고 딱히 이 List라는 애에서 어마어마한 다른 작업을 하고 있는 건 아니었으니까, 정상적으로 실행될 확률이 높다. 여기까지만 실험을 해보자.

GenPackets의 Test 클래스 전까지 복사를 한 다음에 ServerSession과 ClientSession에 넣어준다.

그 다음에 클라에서 서버로 데이터를 보내는 식으로 만들어 줬으니까, ServerSession의 OnConnected에서 간단하게 atrribute를 보내는 실습을 해보자.

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

    PlayerInfoReq packet = new PlayerInfoReq() { playerId = 1001, name = "ABCD" };
    packet.skills.Add(new PlayerInfoReq.Skill() { id = 101, level = 1, duration = 3.0f });
    packet.skills.Add(new PlayerInfoReq.Skill() { id = 201, level = 2, duration = 4.0f });
    packet.skills.Add(new PlayerInfoReq.Skill() { id = 301, level = 3, duration = 5.0f });
    packet.skills.Add(new PlayerInfoReq.Skill() { id = 401, level = 4, duration = 6.0f });

    // 보낸다
    //for (int i = 0; i < 5; i++)
    {
        ArraySegment<byte> s = packet.Write();

        if (s != null)
            Send(s);
    }
}

에서

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

    PlayerInfoReq packet = new PlayerInfoReq() { playerId = 1001, name = "ABCD" };
    var skill = new PlayerInfoReq.Skill() { id = 101, level = 1, duration = 3.0f};
		skill.attributes.Add(new PlayerInfoReq.Skill.Attribute() { att = 77 }); 
		packet.skills.Add(new PlayerInfoReq.Skill() { id = 101, level = 1, duration = 3.0f });
    packet.skills.Add(new PlayerInfoReq.Skill() { id = 201, level = 2, duration = 4.0f });
    packet.skills.Add(new PlayerInfoReq.Skill() { id = 301, level = 3, duration = 5.0f });
    packet.skills.Add(new PlayerInfoReq.Skill() { id = 401, level = 4, duration = 6.0f });

    // 보낸다
    //for (int i = 0; i < 5; i++)
    {
        ArraySegment<byte> s = packet.Write();

        if (s != null)
            Send(s);
    }
}

이렇게 추가를 해준다.

여기까지 문제가 없을지 살펴 보면

public List<Attribute> attributes = new List<Attribute>();

여기서 문제가 일어나고 있어, 구조체는 인스턴스 속성 또는 Initializer를 사용할 수 없습니다. 라고 나온다. ServerSeesion과 ClientSession의 Attribute와 Skill을 struct에서 class로 바꾼다.

결국

class ServerSession : Session
{
    public override void OnConnected(EndPoint endPoint)
    {
        Console.WriteLine($"OnConnected : {endPoint}");

        PlayerInfoReq packet = new PlayerInfoReq() { playerId = 1001, name = "ABCD" };

        var skill = new PlayerInfoReq.Skill() { id = 101, level = 1, duration = 3.0f};
				skill.attributes.Add(new PlayerInfoReq.Skill.Attribute() { att = 77 });
				packet.skills.Add(skill); 

        **packet.skills.Add(new PlayerInfoReq.Skill() { id = 201, level = 2, duration = 4.0f });
        packet.skills.Add(new PlayerInfoReq.Skill() { id = 301, level = 3, duration = 5.0f });
        packet.skills.Add(new PlayerInfoReq.Skill() { id = 401, level = 4, duration = 6.0f });

skill에다가 이 부분을 넣어 줘가지고, attribute를 넣어준 상태로 skills에 skill을 넣어주면

이제 attribute까지 추가된 skill로 들어갈텐데

반대쪽에서 받을 때 정상적으로 파싱이 되면 성공적인 거.

ClientSession의 OnRecvPacket에서 foreach (PlayerInfoReq.Skill skill in p.skills)에 break point를 잡아가지고, 뭔가가 들어오는지를 살펴 볼거야.

속성에서 여러개의 프로젝트가 시작하게 설정을 바꿔준 다음에 시작을 해보면

p를 보면 skills에 4개가 들어오고 atrributes에 Count가 1로 att값 77까지 정상적으로 들어온 것을 볼 수 있다.

결국 PDL.xml을 정의해 놓은 상태로 패킷 변환이 성공적으로 일어나고 있는 거고,

다만 이렇게 이중 list까지 지원하고 싶으면

PacketFormat을 어떤 식으로든 잘 돌아가게끔 맞춰 주거나,

memberListFormat에서 만들어준 struct 부분을 class로 변환만 해도 돌아가긴 돌아갈거야.

public static string memberListFormat =
@"
public class {0}
{{

이런식으로 바꿔줘도 크게 잘못 된 건 아니다.

 

여기서 중요한 건 결국 우리가 코드를 만들 때 너무 하드코딩을 하는 방식으로 만든게 아니라 이런 식으로 {0}{1}{2}를 하나씩 집어 준 다음에 여기 있는 부분을 동적으로 만들어 줘서 밀어 넣어 줬었어.

이런 식으로 자동화를 하다 보니까 의도치 않았지만 2중 리스트도 정상적으로 처리가 잘 되고 있는 걸 볼 수 있다.

이렇게 해서 패킷 자동화 까지는 여기서 끝낼 거고, 여기서 개선하고 싶다면 조금씩 손을 보면 된다.

 

 

 

작업한 코드

PacekFormat

using System;
using System.Collections.Generic;
using System.Text;

namespace PacketGenerator
{
    class PacketFormat
    {
        // {0} 패킷 이름/번호 목록
        // {1} 패킷 목록
        public static string fileFormat =
@"using System;
using System.Collections.Generic;
using System.Text;
using System.Net; 
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> s = new Span<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);

        success &= BitConverter.TryWriteBytes(s.Slice(count, s.Length - count), (ushort)PacketID.{0});
        count += sizeof(ushort);
        {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} 변수 이름
        // {1} 변수 형식
        public static string readByteFormat =
@"this.{0} = ({1})segment.Array[segment.Offset + count];
count += sizeof({1});";

        // {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 += {0}Len;";

        // {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} 변수 이름
        // {1} 변수 형식
        public static string writeByteFormat =
@"segment.Array[segment.Offset + count] = (byte)this.{0};
count += sizeof({1});";

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

        // {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);";
    }
}

PDL.xml

<?xml version="1.0" encoding="utf-8" ?>
<PDL>
  <packet name ="PlayerInfoReq">
    <byte name ="testByte"/>
    <long name ="playerId"/>
    <string name ="name"/>
    <list name ="skill">
      <int name ="id"/>
      <short name ="level"/>
      <float name ="duration"/>
      <list name ="attribute">
        <int name ="att"/>
      </list>"
    </list>
  </packet>
  <packet name ="Test">
    <int name ="testInt"/>
  </packet>
</PDL>

PacketGenerator의 Program.cs

using System;
using System.IO;
using System.Xml;

namespace 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 r = XmlReader.Create("PDL.xml", settings))
            {
                r.MoveToContent(); 

                while(r.Read())
                {
                    if (r.Depth == 1 && r.NodeType == XmlNodeType.Element)
                        ParsePacket(r); 
                    //Console.WriteLine(r.Name + " " + r["name"]); 
                }

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

        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);
            packetEnums += string.Format(PacketFormat.packetEnumFormat, packetName, ++packetId) + Environment.NewLine + "\\t"; 
        }

        // {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; 

            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 "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, 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:
                        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 Tuple<string, string, string> ParseList(XmlReader r)
        {
            string listName = r["name"];
            if (string.IsNullOrEmpty(listName))
            {
                Console.WriteLine("List without name");
                return null;
            }

            Tuple<string, string, string> t = ParseMembers(r);

            string memberCode = string.Format(PacketFormat.memberListFormat,
                FirstCharToUpper(listName),
                FirstCharToLower(listName),
                t.Item1,
                t.Item2,
                t.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); 
        }

        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 ""; 
            }
        }

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

ServerSession

using System;
using System.Collections.Generic;
using System.Text;
using System.Net; 
using ServerCore;

namespace DummyClient
{
	public enum PacketID
	{
		PlayerInfoReq = 1,
		Test = 2,

	}

	class PlayerInfoReq
	{
		public byte testByte;
		public long playerId;
		public string name;

		public class Skill
		{
			public int id;
			public short level;
			public float duration;

			public class Attribute
			{
				public int att;

				public void Read(ReadOnlySpan<byte> s, ref ushort count)
				{
					this.att = BitConverter.ToInt32(s.Slice(count, s.Length - count));
					count += sizeof(int);
				}

				public bool Write(Span<byte> s, ref ushort count)
				{
					bool success = true;
					success &= BitConverter.TryWriteBytes(s.Slice(count, s.Length - count), this.att);
					count += sizeof(int);
					return success;
				}
			}
			public List<Attribute> attributes = new List<Attribute>();

			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);
				this.attributes.Clear();
				ushort attributeLen = BitConverter.ToUInt16(s.Slice(count, s.Length - count));
				count += sizeof(ushort);
				for (int i = 0; i < attributeLen; i++)
				{
					Attribute attribute = new Attribute();
					attribute.Read(s, ref count);
					attributes.Add(attribute);
				}
			}

			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);
				success &= BitConverter.TryWriteBytes(s.Slice(count, s.Length - count), (ushort)this.attributes.Count);
				count += sizeof(ushort);
				foreach (Attribute attribute in this.attributes)
					success &= attribute.Write(s, ref count);
				return success;
			}
		}
		public List<Skill> skills = new List<Skill>();

		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.testByte = (byte)segment.Array[segment.Offset + count];
			count += sizeof(byte);
			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);

			success &= BitConverter.TryWriteBytes(s.Slice(count, s.Length - count), (ushort)PacketID.PlayerInfoReq);
			count += sizeof(ushort);
			segment.Array[segment.Offset + count] = (byte)this.testByte;
			count += sizeof(byte);
			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.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);
		}
	}

	class ServerSession : Session
    {
        public override void OnConnected(EndPoint endPoint)
        {
            Console.WriteLine($"OnConnected : {endPoint}");

            PlayerInfoReq packet = new PlayerInfoReq() { playerId = 1001, name = "ABCD" };

            var skill = new PlayerInfoReq.Skill() { id = 101, level = 1, duration = 3.0f};
			skill.attributes.Add(new PlayerInfoReq.Skill.Attribute() { att = 77 });
			packet.skills.Add(skill); 

            packet.skills.Add(new PlayerInfoReq.Skill() { id = 201, level = 2, duration = 4.0f });
            packet.skills.Add(new PlayerInfoReq.Skill() { id = 301, level = 3, duration = 5.0f });
            packet.skills.Add(new PlayerInfoReq.Skill() { id = 401, level = 4, duration = 6.0f });

            // 보낸다
            //for (int i = 0; i < 5; i++)
            {
                ArraySegment<byte> s = packet.Write();

                if (s != null)
                    Send(s);
            }
        }

        public override void OnDisconnected(EndPoint endPoint)
        {
            Console.WriteLine($"OnDisconnected : {endPoint}");
        }

        public override int OnRecv(ArraySegment<byte> buffer)
        {
            string recvData = Encoding.UTF8.GetString(buffer.Array, buffer.Offset, buffer.Count);
            Console.WriteLine($"[From Server] {recvData}");
            return buffer.Count;
        }

        public override void OnSend(int numOfBytes)
        {
            Console.WriteLine($"Transferred bytes: {numOfBytes}");
        }
    }
}

ClientSession

using System;
using System.Collections.Generic;
using System.Text;
using System.Net.Sockets;
using System.Threading;
using System.Threading.Tasks;
using ServerCore;
using System.Net;

namespace Server
{
	public enum PacketID
	{
		PlayerInfoReq = 1,
		Test = 2,

	}

	class PlayerInfoReq
	{
		public byte testByte;
		public long playerId;
		public string name;

		public class Skill
		{
			public int id;
			public short level;
			public float duration;

			public class Attribute
			{
				public int att;

				public void Read(ReadOnlySpan<byte> s, ref ushort count)
				{
					this.att = BitConverter.ToInt32(s.Slice(count, s.Length - count));
					count += sizeof(int);
				}

				public bool Write(Span<byte> s, ref ushort count)
				{
					bool success = true;
					success &= BitConverter.TryWriteBytes(s.Slice(count, s.Length - count), this.att);
					count += sizeof(int);
					return success;
				}
			}
			public List<Attribute> attributes = new List<Attribute>();

			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);
				this.attributes.Clear();
				ushort attributeLen = BitConverter.ToUInt16(s.Slice(count, s.Length - count));
				count += sizeof(ushort);
				for (int i = 0; i < attributeLen; i++)
				{
					Attribute attribute = new Attribute();
					attribute.Read(s, ref count);
					attributes.Add(attribute);
				}
			}

			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);
				success &= BitConverter.TryWriteBytes(s.Slice(count, s.Length - count), (ushort)this.attributes.Count);
				count += sizeof(ushort);
				foreach (Attribute attribute in this.attributes)
					success &= attribute.Write(s, ref count);
				return success;
			}
		}
		public List<Skill> skills = new List<Skill>();

		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.testByte = (byte)segment.Array[segment.Offset + count];
			count += sizeof(byte);
			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);

			success &= BitConverter.TryWriteBytes(s.Slice(count, s.Length - count), (ushort)PacketID.PlayerInfoReq);
			count += sizeof(ushort);
			segment.Array[segment.Offset + count] = (byte)this.testByte;
			count += sizeof(byte);
			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.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);
		}
	}

	//class Packet
	//{
	//    public ushort size;
	//    public ushort packetId;
	//}

	//class PlayerInfoReq : Packet    // 클라에서 서버로: 나는 플레이어 정보를 알고 싶어. 그 정보는 playerId라는 플레이어야.
	//{
	//    public long playerId;
	//}

	//class PlayerInfoOk : Packet     // 서버에서 클라로: 답변을 주는 거. 
	//{
	//    public int hp;
	//    public int attack;    // hp와 attack을 반환한다고 가정을 해보자. 간단하게 상황을 만들어 준 거. 
	//}

	class ClientSession : PacketSession
    {
        public override void OnConnected(EndPoint endPoint)
        {
            Console.WriteLine($"OnConnected : {endPoint}");

            //Packet packet = new Packet() { size = 100, packetId = 10 };

            //ArraySegment<byte> openSegment = SendBufferHelper.Open(4096);
            //byte[] buffer = BitConverter.GetBytes(packet.size);
            //byte[] buffer2 = BitConverter.GetBytes(packet.packetId);
            //Array.Copy(buffer, 0, openSegment.Array, openSegment.Offset, buffer.Length);
            //Array.Copy(buffer2, 0, openSegment.Array, openSegment.Offset + buffer.Length, buffer2.Length);
            //ArraySegment<byte> sendBuff = SendBufferHelper.Close(buffer.Length + buffer2.Length);

            //Send(sendBuff);
            Thread.Sleep(5000);
            Disconnect();
        }
        public override void OnRecvPacket(ArraySegment<byte> buffer)
        {
            ushort count = 0;

            ushort size = BitConverter.ToUInt16(buffer.Array, buffer.Offset);
            count += 2;
            ushort id = BitConverter.ToUInt16(buffer.Array, buffer.Offset + count);
            count += 2;

            switch ((PacketID)id)
            {
                case PacketID.PlayerInfoReq:
                    {
                        //long playerId = BitConverter.ToInt64(buffer.Array, buffer.Offset + count);
                        //count += 8; // 나중에 이어서 파싱을 해야 할 경우를 대비해서 맞춰준다.
                        PlayerInfoReq p = new PlayerInfoReq();
                        p.Read(buffer);
                        Console.WriteLine($"PlayerInfoReq: {p.playerId} {p.name}");

                        foreach (PlayerInfoReq.Skill skill in p.skills)
                        {
                            Console.WriteLine($"Skill({skill.id})({skill.level})({skill.duration})");
                        }
                    }
                    break;
            }
            Console.WriteLine($"RecvPacketId: {id}, Size: {size}");
        }

        public override void OnDisconnected(EndPoint endPoint)
        {
            Console.WriteLine($"OnDisconnected : {endPoint}");
        }

        public override void OnSend(int numOfBytes)
        {
            Console.WriteLine($"Transferred bytes: {numOfBytes}");
        }
    }

}

 

확인 테스트

Q1. 이번 시간에 한 작업들을 나열해 보세요.

 

1. 파일 자체에 대한 포멧 정의 하기
-> PacketFormat.cs에서 public static string fileFormat =@"";을 선언한다. 
ServerSession에서 사용하고 있는 using과 enum PacketID를 "" 안에 복제한다. 소괄호를 2개로 맞춘다.
PacketID부분은 {0}으로, 아래 class PlayerInfoReq의 패킷 목록들이 들어갈 부분은 {1}로 해준다. 
{0}를 위해 public static string packetEnumFormat = @"{0} = {1},";를 선언한다.

2. 정의한 포멧대로 코드 생성 하기
-> PacketFormat에서 추가한 내용들이 적용되게 하기 위해 PacketGenerator의 Program으로 가서 
File.WriteAllText("GenPackets.cs", genPackets);이렇게 파일로 넣어주기 전에 
string.Format(PacketFormat.fileFormat, "", genPackets); 이 코드를 추가해 준다. ""안에 패킷 이름/번호 목록을 넣는다. ""안에 넣을 값을 구하기 위해
static string packetEnums;를 선언한다. 
패킷을 몇개 처리 했는지 기억하기 위해 static ushort packetId;을 선언한다. 
packetEnums를 string.Format(PacketFormat.fileFormat, packetEnums, genPackets); 이렇게 ""자리에 넣는다. 
string fileText로 string.Format의 반환 값을 받는다. 
위에서 대입해준 packetEnums를 만들기 위해 genPackets의 ParsePacket으로 가서 genPackets+=를 해주는 마지막 줄 다음에 packetEnums += string.Format(PacketFormat.packetEnumFormat, packetName, ++packetId); 이렇게 packetEnums에 packetEnumFormat 코드를 추가해 준다. 

3. 중간 테스트 하기
-> PacketGenerator를 시작 프로그램으로 설정하고 실행한다. 
GenPackets.cs를 본다. 

4. 패킷이 여러개면 어떻게 될지 실험하기
패킷에 여러개면 어떻게 될지 보기 위해 PDL에 testInt를 가진 Test라는 이름의 패킷을 하나 더 추가한다. 
PDL을 저장후 실행파일이 있는 곳으로 옮긴다. PacketGenerator.exe을 실행한다.
GenPackets에 가서 보면 Test클래스가 추가로 만들어진 것을 볼 수 있다. 

5. 정렬하기
하지만 pubic enum PacketID가 정렬이 잘 안되어 있다. Program의 ParsePacket으로 가서 packetEnums에 줄 바꾸기 코드와 /t코드를 더해준다. 
정렬이 되게 코드가 생성이 된다. 

6. 타입이 byte인 경우 포멧 추가 
-> 생성된 GenPacket.cs의 class PlayerInfoReq에 public byte testByte;를 추가해 testByte라는 인자가 있다고 가정한다. 
PDL에도 <byte name ="testByte"/>를 추가한다. 
Read로 가서 this.testByte = segment.Array[segment.Offset + count];
count += sizeof(byte); 이렇게 추가한다. 
Write에 가서  segment.Array[segment.Offset + count] = this.testByte;  ;
count += sizeof(byte); 이렇게 추가한다. 
이 코드들이 자동 생성되게 하기 위해 PacketFormat.cs로 가서 
        public static string readByteFormat =@"this.{0} = segment.Array[segment.Offset + count];
count += sizeof({1});"; 이렇게 자동화 코드를 추가
한다. 
write의 경우도         public static string readByteFormat = @"this.{0} = segment.Array[segment.Offset + count];
count += sizeof({1});"; 이렇게 자동화 코드를 추가한다. 

7. 정의한 포멧대로 byte 처리 코드 생성
-> 이 자동화 코드를 사용하기 위해 Program.cs의 ParseMembers의 swich문에서 
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);
이렇게 코드를 생성하게 한다. 

8. byte 처리 코드 생성 테스트
-> PDL을 실행파일이 있는 곳으로 복분한다. 실행후 GenPacket에 Byte 관련 코드가 생성 되었는지 확인한다.

9. sbyte인 경우 챙기기
-> PacketFormat.cs에서 
public static string readByteFormat = @"this.{0} = ({1})segment.Array[segment.Offset + count];
public static string writeByteFormat = @"segment.Array[segment.Offset + count] = (byte)this.{0};
이렇게 안전장치를 마련한다. 

10. 2중 List 지원
-> PDL파일에 skill list안에 int att를 가진 attribute라는 list를 넣는 조작을 한 뒤 실행한다.
실행 후 GemPacket.cs를 열어 보면 Skill 안에 Atrribute가 생긴것을 볼 수 있다. 
생성된 코드가 잘 작동 되는지 실험하기 위해 GenPackets의 Test 클래스 전까지 복사해서 ServerSession, ClienSession에 넣어준다.
ServerSession의 OnConnected에서 
var skill = new PlayerInfoReq.Skill() { id = 101, level = 1, duration = 3.0f};
skill.attributes.Add(new PlayerInfoReq.Skill.Attribute() { att = 77 }); 
이렇게 코드를 추가하면 struct는 인스턴스 할 수 없다고 나오니 
ServerSession의 Attribute structure와 Skill stucture를 class로 바꿔준다.  
packet.skills.Add(skill); 
이렇게 attribute를 넣은 skill을 skills에 넣는다. 
ClientSession의 OnRecvPacket에서 foreach (PlayerInfoReq.Skill skill in p.skills)에 break point를 잡고 여러 프로젝트 실행으로 설정 후 실행한다. 
PlayerInfoReq를 생성할 때 skills가 중에서 첫번째 것을 보면 Count가 1일고 값이 77로 잘 들어온 것을 확인한다.
코드가 class로 생성되게 하기 위해 PacketFormat.cs에서 memberListFormat = @""안에서 public struct {0}를 public class {0}으로 수정한다.


반응형

댓글