Server programming

03_11_패킷 직렬화_PacketGenerator #6_PacketManager 코드 생성을 자동화, 패킷 분리

devRiripong 2023. 4. 20.
반응형

다룰 내용 및 요약

PacketManager 코드 생성을 자동화하는 방법을 설명합니다. 다음과 같은 단계를 따릅니다.

  1. PacketFormat 맞추기
  2. PacketGenerator의 Program.cs에서 PacketFormat을 사용하여 코드 생성
  3. GenPackets.bat 수정
  4. 에러 수정
  5. DummyClient 대칭 작업
  6. 패킷 분리

이 프로세스는 PacketManager의 코드 생성을 자동화하고, 각 클라이언트와 서버 간 패킷을 분리하여 관리를 용이하게 합니다. 이로써 서버의 분산 처리, 해킹 대비, 패킷 사용범위 구분 등이 가능해집니다. 이해하는 것이 중요하며, 이를 바탕으로 다양한 환경에서 최적화를 할 수 있습니다.

의문점: 왜 PacketHandler는 자동으로 만들어 주지 않는 건가요?

→ PacketHandler를 자동으로 생성되는 코드에 넣지 않는 이유는 PacketHandler가 주로 서버 로직과 관련된 내용을 포함하기 때문입니다. 즉, 각 패킷에 대한 실제 처리 로직이 포함되어 있으며, 이는 프로젝트나 사용자의 요구 사항에 따라 독특하고 다양할 수 있습니다.

자동화를 통해 생성되는 코드는 주로 프로토콜 정의, 패킷의 인코딩/디코딩 등과 같이 변경되지 않고 일정한 패턴을 가지는 코드를 생성하는 데에 더 적합합니다. 이렇게 일정한 패턴을 가지는 코드를 자동화하면 개발자들이 직접 작성해야 할 코드의 양을 줄일 수 있고, 실수를 방지할 수 있습니다.

반면에 PacketHandler는 서버의 비즈니스 로직에 따라 다양한 처리를 해야하기 때문에 자동화하기 어렵습니다. 각 패킷마다 처리 방식이 다를 수 있고, 경우에 따라 프로젝트의 요구 사항이 변경될 때마다 수정이 필요할 수도 있습니다. 따라서, 자동화로 생성되는 코드에 PacketHandler를 포함시키는 것은 적절하지 않습니다.

 

전 시간에 만들어준 PacketManager 코드 생성을 자동화 하는 작업을 할 것이다

기존에 PacketFormat이란 애를 만들어 줬으니까 빠르게 만들어 줄 수 있을 것이다.

GenPacket에 같이 끼워 넣어서 만들어도 되지만 이정도 규모라면 별도의 파일로 분리해서 진행할 것이다.

1. PacketForamt 맞추기

일단은 PacketFormat에 가가지고, 윗쪽에 하나를 더 만들어주도록 할건데

managerFormat을 선언하고

public static string managerFormat =
@"
";

여기에 class PacketManager의 코드 전체를 복붙해서 시작을 한다.

				// {0} 패킷 등록
        public static string managerFormat =
@"class PacketManager
{{
    #region Singleton
    static PacketManager _instance; 
    public static PacketManager Instance
    {{
        get
        {{
            if (_instance == null)
                _instance = new PacketManager();
            return _instance; 
        }}
    }}
    #endregion

    Dictionary<ushort, Action<PacketSession, ArraySegment<byte>>> _onRecv = new Dictionary<ushort, Action<PacketSession, ArraySegment<byte>>>();
    Dictionary<ushort, Action<PacketSession, IPacket>> _handler = new Dictionary<ushort, Action<PacketSession, IPacket>>(); 

    public void Register()
    {{
{0}
    }}

    public void OnRecvPacket(PacketSession session, 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;

        Action<PacketSession, ArraySegment<byte>> action = null;
        if (_onRecv.TryGetValue(id, out action))
            action.Invoke(session, buffer); 
    }}

    void MakePacket<T>(PacketSession session, ArraySegment<byte> buffer) where T : IPacket, new()
    {{
        T pkt = new T();
        pkt.Read(buffer);

        Action<PacketSession, IPacket> action = null;
        if (_handler.TryGetValue(pkt.Protocol, out action))
            action.Invoke(session, pkt); 
    }}
}}
";

기존과 마찬가지로 중괄호를 하나씩 늘려줘야 한다.

자동화 될 코드를 살펴보면 의외로 많이 없다는 걸 알 수 있다.

Register하는 이 부분만 자동화 하는 코드가 들어간다는 말이 되는 거니까 얘를 **{0}**으로 찝어준다.

public void Register()
    {{
{0}
        _onRecv.Add((ushort)PacketID.PlayerInfoReq, MakePacket<PlayerInfoReq>); 
        _handler.Add((ushort)PacketID.PlayerInfoReq, PacketHandler.PlayerInfoReqHandler); 
    }}

결국 여기 있는 부분을 밖으로 빼야 한다는 얘기

ctrl+x로 잘라내기를 한 상태에서 아래다가 하나를 더 파주기를 할건데

public static string managerRegisterFormat =
@"
_onRecv.Add((ushort)PacketID.PlayerInfoReq, MakePacket<PlayerInfoReq>); 
_handler.Add((ushort)PacketID.PlayerInfoReq, PacketHandler.PlayerInfoReqHandler); 
";

managerRegisterFormat을 만들어서 Register에 있던 코드를 붙여넣기 해준다.

class PacketFormat에서

managerFormat같은 경우는 **{0}**이 하나 밖에 없었는데 패킷 등록하는 부분이었고,

새롭게 만들어준 managerRegisterFormat의 경우는 **{0}**에 패킷 이름이 들어가게 될거다.

        // {0} 패킷 이름
        public static string managerRegisterFormat =
@"
_onRecv.Add((ushort)PacketID.{0}, MakePacket<{0}>); 
_handler.Add((ushort)PacketID.{0}, PacketHandler.{0}Handler); 
";

지금 현 상태에서 그대로

public void Register()
    {{
{0}
    }}

여기에 넣어주게 되면 탭이 맞지 않으니까

// {0} 패킷 이름
        public static string managerRegisterFormat =
@"        _onRecv.Add((ushort)PacketID.{0}, MakePacket<{0}>); 
        _handler.Add((ushort)PacketID.{0}, PacketHandler.{0}Handler); 
";

탭을 두번을 해서 여기서 강제로 맞춰 주도록 한다.

패킷 포멧을 이정도 맞춰 줬으면 이제는 PacketGenerator의 Program.cs로 가서 코드를 추가해 줘야 하는데

namespace PacketGenerator
{
    class Program
    {
        static string genPackets;
        static ushort packetId; 
        static string packetEnums;

        static string managerRegister;

기존과 마찬가지로 managerRegister를 추가해 추적 해주도록 하자.

어자피 managerFormat은

static void Main(string[] args)
{
    string pdlPath = "../PDL.xml"; 

    XmlReaderSettings settings = new XmlReaderSettings()
    {
        IgnoreComments = true,
        IgnoreWhitespace = true 
    };

    if (args.Length >= 1)
        pdlPath = args[0];

    using (XmlReader r = XmlReader.Create(pdlPath, 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);**
    }
}

여기서 한번에 만들어 주면 된다.

만약에 패킷을 분석해서 다 조립을 완료 했다고 하면 최종적으로 string managerText를 만들어 주면 되는데

string managerText = string.Format(PacketFormat.managerFormat, managerRegister);

방금 추가한 managerRegister를 붙여주면 된다.

그러면 string.Format에 의해서 알아서 조립이 될거고,

File.WriteAllText("PacketManager.cs", managerText);

그 다음에 PacketManager.cs에 조립된 코드를 넣어주면 된다.

 

남은 건 managerRegister에 들어갈 Regist하는 코드를 패킷을 돌면서 추가를 해줘야 하는데

ParsePacket이라는 부분 아래다가 이어서 뭔가를 넣어주면 된다.

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

참고로 packetEnums는 tab을 붙여줬는데 managerRegister는 안붙인 이유는

// {0} 패킷 이름
        public static string managerRegisterFormat =
@"        _onRecv.Add((ushort)PacketID.{0}, MakePacket<{0}>); 
        _handler.Add((ushort)PacketID.{0}, PacketHandler.{0}Handler); 
";

두 줄짜리니까 tab을 두번 넣어줄 수 없을거야. 두번에 걸쳐서 파싱한다거나, 건너띈 값을 붙인다거나 다양한 방법을 이용해야 하니까 그게 귀찮아서 그냥 수동으로 탭탭을 붙여 준거.

그래서 mangerRegister 에는 tab을 안붙였다. 중요한 부분은 아냐.

 

여기까지 해서 PacketGenerator를 빌드를 해주면,

이제는 .bat파일만 다시 한번 돌려 줘가지고, 걔가 잘 실행이 되면 된다.

.bat 파일도 수정을 해야 한다.

START ../../PacketGenerator/bin/PacketGenerator.exe ../../PacketGenerator/PDL.xml
XCOPY /Y GenPackets.cs "../../DummyClient/Packet"
XCOPY /Y GenPackets.cs "../../Server/Packet"
XCOPY /Y PacketManager.cs "../../DummyClient/Packet"
XCOPY /Y PacketManager.cs "../../Server/Packet"

이렇게 하면 PacketManager.cs도 각 경로에 복사를 하게 될거야.

이제 GenPackets.bat을 더블클릭 해보면, Server에도 DummyClient에도 PacketManager.cs가 생성이 된 것을 볼 수 있다.

4. 에러 수정

4_1 에러 수정1: PacketFormat에 using 추가

생성된 PacketManagers.cs 파일을 보면 using을 빼먹고 왔는 걸 알 수 있다.

에러 발생한걸 하나하나 잠재적 수정사항 기능을 이용해 추가해준 다음에 얘를 아까 정의했던 PacketFormat에다가 추가하는 방법을 알아보자.

PacketHandler에 뜨는 에러는 뜨는게 정상이다. DummyClient 쪽에는 PacketHandler를 안만들어 줬기 떄문에 정상적인 거.

PacketFormat으로 가서

public static string managerFormat =
@"using ServerCore;
using System;
using System.Collections.Generic;

이렇게 using 3개를 넣어주자.

그 다음에 PacketGenerator를 다시 한번 빌드를 해주고,

genPackets.bat을 다시 한번 실행해 주도록 한다.

4_2 에러 수정2: Server의 PacketHandler 코드 안의 nameSpace Server 삭제

생성된 PacketManager에 다시 가보면 PacketHandler에 밑줄이 그어져 있는 것을 볼 수 있는데

Server가 빠져서 그런데, Server쪽에 만들어 놓은 PacketHandler에다가 namespace Server를 붙여놔서 그런거다. 일단 이걸 제거해 준다.

 

굳이 PacketManager에 using Server를 추가하게 자동화하지 않은 이유는

DummyClient에서도 똑같은 파일을 사용하고 있는데 PacketHandler를 namespace Server로 하는게 이상하니까 제거를 한건데 공용적인 다른 이름의 namespace를 사용해도 크게 상관은 없다.

어쨌든 이렇게 하면 PacketHandler에 있는 빨간줄은 사라지고,

4_2에러 수정3: PacketHandler 코드 안에 PlayerInfoReqHandler와 같은 인터페이스로 TestHandler추가

생성된 PacketManager.cs의 Register에

public void Register()
{
    _onRecv.Add((ushort)PacketID.PlayerInfoReq, MakePacket<PlayerInfoReq>); 
    _handler.Add((ushort)PacketID.PlayerInfoReq, PacketHandler.PlayerInfoReqHandler); 

    _onRecv.Add((ushort)PacketID.Test, MakePacket<Test>); 
    _handler.Add((ushort)PacketID.Test, PacketHandler.**TestHandler**); 
}

TestHandler는 정의한 적이 없으니까 빨간줄이 뜨는거.

얘도 마찬가지로 PacketHandlerPlayerInfoReqHandler 함수와 똑같은 인터페이스로

public static void TestHandler(PacketSession session, IPacket packet)
{

}

이렇게 추가를 해주면 그 문제는 해결이 된다.

결국 PDL에다 패킷을 추가를 한 다음에는 까먹지 않고 PacketHandler에다가 해당 핸들러 함수를 추가를 해줘야 된다는 것만 잊지 않으면 된다.

그럼 PacketManager.cs 쪽에는 아무런 문제가 없다는 것을 볼 수 있다.

5. DummyClient 대칭 작업

DummyClient쪽에서도 똑같은 작업을 해주긴 해야 한다.

DummyClient의 Packet 폴더에 새 항목 추가를 해서 PacketHandler라는 클래스를 추가하고,

ServerPacketHandler인터페이스를 복붙을 하고 시작을 하자.

이제 양쪽에 대칭적으로 준비가 된 거.

이 상태에서 잘 되는지 테스트를 해보면, 솔루션 속성에 가서 여러 개의 시작 프로젝트를 누르고

f5를 누르면 크게 변한 건 없으니까 잘 출력이 되는 걸 볼 수 있다.

진짜 어느정도 윤곽이 나온거.

처음에 이런식으로 시간을 들여가지고 자동화를 편리하게 해 놔야지만 나중에 인생이 편해진다.

그게 아니었으면 일일이 ClientSession에 switch case문을 추가하고 있었을 거야.

이제 그 부분을 어느정도 자동화 해가지고 PacketHandler에다만 코드를 추가 하면 되니까 이제 많이 편해진 것을 알 수 있다.

이제 PacketHandler로 실제 패킷 처리를 넘기는 것 까지는 굉장히 잘 처리하고 있고, 굉장히 유용하고 편리할 거 같다는 생각이 든다.

6. 패킷 분리

마지막으로 하나만 더 수정해야 할 게 있는데 지금 단계에서 컨텐츠를 만들지 않았을 때는 와닿지 않을 수 있겠지만 나중에 패킷을 설계 할 때 곰곰이 생각을 해보면 양방향으로 패킷이 가는 경우는 거의없다.

패킷 분리 필요한 이유 1 - 안쓰는 코드 만드는 거 찜찜해

무슨 말이냐 하면 예를 들어

<?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>

예를 들어 PlayerInfoReq라는 패킷을 하나 만들었는데 얘 같은 경우에는 클라에서 서버쪽으로 보내는 패킷이 될거야. 서버에서 클라 쪽으로 플레이어의 정보를 주세요 라는 요청을 넘길 일은 없을테니까 얘는 사실은 클라에서 서버 쪽으로 가는 패킷이 될거야. 우리는 지금 별도로 그런 구분을 하고 있지 않으니까 양쪽 PacketHandler에 PlayerInfoReqHandler가 들어가 있다.

만약에 내가 클라에서 서버로 전송할 용도로만 사용 할건데도 불구하고 양쪽에다 이렇게 함수를 만들어가지고, 비어 놓더라도 선언해야 한다는게 찝찝한 부분이다.

패킷 분리 필요한 이유 2 - 분산 서버를 쓸 때 분리 해야 해

그리고 이런 귀찮은 문제 말고도 다른 문제가 하나 더 있는데 나중에 우리가 클라랑 서버 이렇게 둘 끼리만 소통을 하는게 아니라 서버도 분산처리를 하는 경우가 간혹 있기 때문에 예를 들어 스킬과 같이 부하가 심한 것은 world server나 zone server등으로 게임을 연산하는 서버를 하나 두고, 그 다음에 DB 처리를 하는 다른 용도의 서버는 윗단에 둬서 결국 A라는 서버와 B라는 서버가 왔다 갔다 통신을 하면서 걔가 하나의 서버군을 조성하고 있는 그런 방식으로 만들게 되는데 그런데도 이런식으로 패킷 분리를 어느정도 해야한다.

패킷 분리 필요한 이유 3 - 해킹 위험이 있어

예전에 선생님이 일할 때 A라는 B라는 두 서버끼리만 통신하는 패킷을 파고 있었는데, 어느정도 신용할 수 있고 해킹 위험이 없다고 판단했기 때문에 대부분 거기는 검사를 타이트 하게 안하는 경향이 있었어. 나중에 어떤 해커가 그 사람은 모르고 했겠지만 이런 저런 패킷을 조작하고 온갖 패킷을 쏴보는 도중에 하필이면 클라에서 쏜 패킷이 알고 보니까 패킷 아이디가 A랑 B사이에 통신하려고 만들었던 그 패킷 아이디여가지고 걔가 뭔가 검증을 절묘하게 통과를 해서 크래쉬를 유발한사건이 있었다.

결국 Handler를 팔 때는 용도를 잘 구분을 해놓는게 좋다. PlayerInfoReq라는 걸 만들었지만 여기에 어떤 식으로든 얘가 어디서 사용될 것인지, 클라에서 서버로 사용될 거인지, 서버끼리 사용될 것인지 등등을 구분을 해가지고, 안전 처리를 미리미리 해놓고,

그리고 결국 PacketMangerRegister에서 패킷을 등록을 할 때 온갖 패킷을 다 등록하는게 아니라 딱 자기가 필요한 것만 추가를 해야지만 그런 최악의 상황을 예방할 수 있겠다는게 된다.

다양한 방법으로 할 수 있다.

6_1. PDL에서 패킷 구분하기

예를 들면 PDL에서

<?xml version="1.0" encoding="utf-8" ?>
<PDL>
  <packet name ="PlayerInfoReq" usage="">
    <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>

usage라고 해서 추가해도 되고,

그게 아니라 심플한 걸 좋아하면

<?xml version="1.0" encoding="utf-8" ?>
<PDL>
  <packet name ="C_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 ="S_Test">
    <int name ="testInt"/>
  </packet>
</PDL>

C_, T_ 를 둬가지고,

**C_**가붙은 컨벤션은 클라에서 서버쪽으로 보내는패킷이 될거고,

반대로 **S_**가 붙은 건 서버에서 클라로 보내는 패킷을 이렇게 정의 할거다.

6_2. PacketManager 코드 생성 자동화를 할 때 각각 필요한 코드만 생성되게 하기.

결국엔 이 다음에 우리가 해야할 것은

PacketManager를 자동화 할 때 Server쪽에 추가되는 PacketManager의 경우에는

PacketManager에서 Register를 할 때

클라에서 보낸 PDL에서 C_가 붙은 애들만

public void Register()
{
    **_onRecv.Add((ushort)PacketID.PlayerInfoReq, MakePacket<PlayerInfoReq>);** 
    **_handler.Add((ushort)PacketID.PlayerInfoReq, PacketHandler.PlayerInfoReqHandler);** 
    _onRecv.Add((ushort)PacketID.Test, MakePacket<Test>); 
    _handler.Add((ushort)PacketID.Test, PacketHandler.TestHandler); 
}

등록을 해야 하는 거고,

반대로 DummyClient 쪽의 PacketManager같은 경우에는 S_가 붙은 애들만

public void Register()
{
    _onRecv.Add((ushort)PacketID.PlayerInfoReq, MakePacket<PlayerInfoReq>); 
    _handler.Add((ushort)PacketID.PlayerInfoReq, PacketHandler.PlayerInfoReqHandler); 
    **_onRecv.Add((ushort)PacketID.Test, MakePacket<Test>); 
    _handler.Add((ushort)PacketID.Test, PacketHandler.TestHandler);** 
}

여기다 등록을 해야 한다.

그래서 그 작업을 추가를 해주도록 할거야.

 

이거를 추가를 하기 위해서는 다양한 방법이 있겠지만

간단하게 하는 방법은 PacketGenerator의 Program에 가서,

애당초 패킷 네임을 어떤 것인지를 구분을 해가지고 분리해서 파일을 만드는 거.

결국

static string managerRegister;

managerRegister 라는 얘가 결국은 PacketManager를 등록하고 있었는데

얘를 이제 두개로 구분을 할겁니다.

static string clientRegister;
static string serverRegister;

이렇게 두개의 파일로 구분을 해서 만들겠다는 의도가 된다.

그러면

static void Main(string[] args)
{
    string pdlPath = "../PDL.xml"; 

    XmlReaderSettings settings = new XmlReaderSettings()
    {
        IgnoreComments = true,
        IgnoreWhitespace = true 
    };

    if (args.Length >= 1)
        pdlPath = args[0];

    using (XmlReader r = XmlReader.Create(pdlPath, 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);
        **string managerText = string.Format(PacketFormat.managerFormat, managerRegister);
        File.WriteAllText("PacketManager.cs", managerText);** 

    }
}

에러가 나고 있는데 이 부분을 두개로 만들어 주겠다는 게 된다.

string clientManagerText = string.Format(PacketFormat.managerFormat, clientRegister);
File.WriteAllText("ClientPacketManager.cs", clientManagerText);
string serverManagerText = string.Format(PacketFormat.managerFormat, serverRegister);
File.WriteAllText("ServerPacketManager.cs", serverManagerText);

이렇게 구분을 해서 만들어 준다.

 

그리고 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);
    packetEnums += string.Format(PacketFormat.packetEnumFormat, packetName, ++packetId) + Environment.NewLine + "\\t";
    
    **if(packetName.StartsWith("S_") || packetName.StartsWith("s_")) 
        clientRegister += string.Format(PacketFormat.managerRegisterFormat, packetName) + Environment.NewLine;
    else
        serverRegister += string.Format(PacketFormat.managerRegisterFormat, packetName) + Environment.NewLine;**         
}

이것만 추가하면 나머지는 별로 고칠 필요가 없다.

그냥 서버랑 클라쪽 패킷만 구분을 해가지고 등록을 하겠다는 의미가 된다.

이 상태에서 PacketGenerator를 한번 빌드 해준 다음에

Common의 Packet 폴더로 가서 PacketManager는 사용 안할 거니까 삭제를 해준 다음에

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"

ClientPacketManager는 Cient 쪽으로 보내고

ServerPacketManager는 Server 쪽으로 보낸다.

이 상태에서 다시 한번 GenPackets.bat 파일을 실행해가지고

이렇게 ClientPacketManager, ServerPacketManager가 생성 된 것을 알 수 있다.

PacketManager는 이제 안쓰니 삭제해 줬다.

 

결국에는 우리가 원한대로 Client 쪽에서는 ClientPacketManager를 보면

public void Register()
{
    _onRecv.Add((ushort)PacketID.S_Test, MakePacket<S_Test>); 
    _handler.Add((ushort)PacketID.S_Test, PacketHandler.S_TestHandler); 

}

ㅇ이렇게 S_ 가 붙은 애들만 추가가 된 거고,

 

반대로 ServerpacketManager 에서는

public void Register()
{
    _onRecv.Add((ushort)PacketID.C_PlayerInfoReq, MakePacket<C_PlayerInfoReq>); 
    _handler.Add((ushort)PacketID.C_PlayerInfoReq, PacketHandler.C_PlayerInfoReqHandler); 

}

C_가 붙은 애만 추가가 된 것을 볼 수 있다.

6_3 PacketHandler를 수정

물론 이렇게 바뀜에 따라서 PacketHandler를 수정을 해주긴 해야 한다.

C_PlayerInfoReqHandler를 복사한 다음에

ServerPacketHandler로 가서

class PacketHandler
{
    public static void PlayerInfoReqHandler(PacketSession session, IPacket packet)
    {
        PlayerInfoReq p = packet as PlayerInfoReq;

        Console.WriteLine($"PlayerInfoReq: {p.playerId} {p.name}");

        foreach (PlayerInfoReq.Skill skill in p.skills)
        {
            Console.WriteLine($"Skill({skill.id})({skill.level})({skill.duration})");
        }
    }

    public static void TestHandler(PacketSession session, IPacket packet)
    {

    }

}

TestHandler는 필요가 없으니 삭제하고

class PacketHandler
{
    public static void C_PlayerInfoReqHandler(PacketSession session, IPacket packet)
    {
        C_PlayerInfoReq p = packet as C_PlayerInfoReq;

        Console.WriteLine($"PlayerInfoReq: {p.playerId} {p.name}");

        foreach (C_PlayerInfoReq.Skill skill in p.skills)
        {
            Console.WriteLine($"Skill({skill.id})({skill.level})({skill.duration})");
        }
    }
}

이렇게 수정을 해줘야 한다.

그리고 마찬가지로 DummyClient 쪽으로 가서

class PacketHandler
{
    public static void PlayerInfoReqHandler(PacketSession session, IPacket packet)
    {

    }

    public static void TestHandler(PacketSession session, IPacket packet)
    {

    }
}

PlayerInfoReqHandler는 이제사용 안할테니 삭제하고

class PacketHandler
{
    public static void S_TestHandler(PacketSession session, IPacket packet)
    {

    }
}

이렇게 만들어 줘야지 정상적으로 처리가 된다.

이렇게 해서 한번더 빌드를 해볼건데 바뀐 것에 따라서 에러가 나는 부분이 있을 것이다.

솔루션 빌드를 하면, 에러가 발생하는데

DummyClientServerSession에 가서 PlayerInfoReq에 C_를 붙여준다.

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

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

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

이렇게 수정해 주고 다시 한번 빌드를 해주면 정상적으로 빌드가 된다.

한번 실행을 해보면

어쨌든 이렇게해서 client랑 server쪽 핸들링 하는 것도 우아하게 자동화 처리를 해봤고,

오늘까지가 패킷을 다루는 마지막 수업이라고 할 수 있다.

 

참고로 마치기 전에 몇개만 짚어 드리자면

Serialize하는 부분, 패킷을 직렬화 하는 이 일련의 단계들이 사실은 가장 기초적으로 한 부분들이다. 가장 간단하게 만들었어. 가장 간단하게 직관적으로 만들었다는 것은

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

사실 이거보다 성능을 뛰어나게 만들 수 있다는 말이 되는데

이를테면 사실은 매번마다 영역을 집어주고, 체크를 하고 더하고 이런걸 하고 있었지만 사실은 이걸 우아하게 하는 방법들이 여러가지 있기는 하다.

근데 여기서 다루기에는 너무 고급 주제들이고, 나중에 C++ 서버를 다룰 때는 이거보다 더 최적화 해서 할 수 있는 방법에 대해 알아볼건데 일단은 인디게임 수준으로 만든다고 가정하면 이정도도 굉장히 훌륭하고 충분하다 생각한다.

어째든 이렇게 자동화하는 거 까지, 그리고 이렇게 패킷을 밀어 넣고 받는 부분에 대해서는 지겹게 했으니까 나중에 혹시라도 우리가 직접 한땀 한땀 만드는 패킷이 아니라 구글 프로토 버퍼 라든가, 플랫 버퍼를 사용한다 하더라도 이런 일련의 과정들을 거친다는 걸 알면 될 거 같아.

자체를 만드는게 중요한게 아니라 어떻게 돌아가는지 어떤 어려움들이 있는지를 이해하는게 가장 중요 했다고 볼 수 있다.

 

 

 

작업 해준 코드들

PacketGenerator/PacketFormat.cs

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

namespace PacketGenerator
{
    class PacketFormat
    {
        // {0} 패킷 등록
        public static string managerFormat =
@"using ServerCore;
using System;
using System.Collections.Generic;

class PacketManager
{{
    #region Singleton
    static PacketManager _instance; 
    public static PacketManager Instance
    {{
        get
        {{
            if (_instance == null)
                _instance = new PacketManager();
            return _instance; 
        }}
    }}
    #endregion

    Dictionary<ushort, Action<PacketSession, ArraySegment<byte>>> _onRecv = new Dictionary<ushort, Action<PacketSession, ArraySegment<byte>>>();
    Dictionary<ushort, Action<PacketSession, IPacket>> _handler = new Dictionary<ushort, Action<PacketSession, IPacket>>(); 

    public void Register()
    {{
{0}
    }}

    public void OnRecvPacket(PacketSession session, 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;

        Action<PacketSession, ArraySegment<byte>> action = null;
        if (_onRecv.TryGetValue(id, out action))
            action.Invoke(session, buffer); 
    }}

    void MakePacket<T>(PacketSession session, ArraySegment<byte> buffer) where T : IPacket, new()
    {{
        T pkt = new T();
        pkt.Read(buffer);

        Action<PacketSession, IPacket> action = null;
        if (_handler.TryGetValue(pkt.Protocol, out action))
            action.Invoke(session, pkt); 
    }}
}}
";

        // {0} 패킷 이름
        public static string managerRegisterFormat =
@"        _onRecv.Add((ushort)PacketID.{0}, MakePacket<{0}>); 
        _handler.Add((ushort)PacketID.{0}, PacketHandler.{0}Handler); 
";

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

public enum PacketID
{{
    {0}
}}

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

{1}
";

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

        // {0} 패킷 이름
        // {1} 멤버 변수
        // {2} 멤버 변수 Read
        // {3} 멤버 변수 Write

        // 여러줄에 걸쳐서 문자열을 정의해 주고 싶을 땐 @를 붙이면 된다. 
        public static string packetFormat =
@"
class {0} : IPacket
{{
    {1}     

    public ushort Protocol {{ get {{ return (ushort)PacketID.{0}; }} }}

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

PacketGenerator/PDL.xml

<?xml version="1.0" encoding="utf-8" ?>
<PDL>
  <packet name ="C_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 ="S_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 string clientRegister;
        static string serverRegister; 

        static void Main(string[] args)
        {
            string pdlPath = "../PDL.xml"; 

            XmlReaderSettings settings = new XmlReaderSettings()
            {
                IgnoreComments = true,
                IgnoreWhitespace = true 
            };

            if (args.Length >= 1)
                pdlPath = args[0];

            using (XmlReader r = XmlReader.Create(pdlPath, 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);
                string clientManagerText = string.Format(PacketFormat.managerFormat, clientRegister);
                File.WriteAllText("ClientPacketManager.cs", clientManagerText);
                string serverManagerText = string.Format(PacketFormat.managerFormat, serverRegister);
                File.WriteAllText("ServerPacketManager.cs", serverManagerText); 
            }
        }

        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";
            
            if(packetName.StartsWith("S_") || packetName.StartsWith("s_")) 
                clientRegister += string.Format(PacketFormat.managerRegisterFormat, packetName) + Environment.NewLine;
            else
                serverRegister += string.Format(PacketFormat.managerRegisterFormat, packetName) + Environment.NewLine;         
        }

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

DummyClient/ServerSession.cs

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

namespace DummyClient
{

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

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

            packet.skills.Add(new C_PlayerInfoReq.Skill() { id = 201, level = 2, duration = 4.0f });
            packet.skills.Add(new C_PlayerInfoReq.Skill() { id = 301, level = 3, duration = 5.0f });
            packet.skills.Add(new C_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}");
        }
    }
}

DummyClient/PacketHandler.cs

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

class PacketHandler
{
    public static void S_TestHandler(PacketSession session, IPacket packet)
    {

    }
}

Server/PacketHandler.cs

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

class PacketHandler
{
    public static void C_PlayerInfoReqHandler(PacketSession session, IPacket packet)
    {
        C_PlayerInfoReq p = packet as C_PlayerInfoReq;

        Console.WriteLine($"PlayerInfoReq: {p.playerId} {p.name}");

        foreach (C_PlayerInfoReq.Skill skill in p.skills)
        {
            Console.WriteLine($"Skill({skill.id})({skill.level})({skill.duration})");
        }
    }
}

Common/Packet/GenPacket.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"

 

 

 

 

확인 테스트

 

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


PacketManager 코드 생성을 자동화 하는 작업

1. PacketForamt 맞추기
-> PacketFormat에 가서 
public static string managerFormat =
@"
";
""안에 class PacketManager 코드를 복붙한다. 
중괄호를 하나씩 늘려주고, Register함수 안의 패킷을 Dictionary에 등록하는 코드를 {0}으로 집어준다. 
public void Register()
    {{
{0}
    }}

안의 원래 코드를 ctrl+x로 잘라내고 
public static string managerRegisterFormat =
@"
_onRecv.Add((ushort)PacketID.PlayerInfoReq, MakePacket<PlayerInfoReq>); 
_handler.Add((ushort)PacketID.PlayerInfoReq, PacketHandler.PlayerInfoReqHandler);";
이렇게 managerRegiaterFormat을 선언하고 안에 붙여 넣어 준다.
그리고 패킷 이름이 들어가게 되는 부분을 {0}으로 집어준다. 탭을 눌러서 정렬해준다.
// {0} 패킷 이름
public static string managerRegisterFormat =
@" _onRecv.Add((ushort)PacketID.{0}, MakePacket<{0}>); 
_handler.Add((ushort)PacketID.{0}, PacketHandler.{0}Handler);";


2. PacketGenerator의 Program.cs로 가서 PacketFormat 이용해 코드 생성하기
->코드 추적 용도로 static string managerRegister;를 추가해 줍니다.
Main에서 패킷을 분석에 조립을 완료 했다고 하면 string.Format에 managerFromat 틀에 추적한 managerRegister문자열을 넣어줘서 managerText를 만들어 줍니다.
string managerText = string.Format(PacketFormat.managerFormat, managerRegister);
그리고 나서 PacketManger.cs파일에 managerText를 넣어 줍니다.
File.WriteAllText("PacketManager.cs", managerText);

이제 managerRegister에 들어갈 코드를 패킷을 돌면서 추가해 줍니다.
ParsePacket에서 해주는데
managerRegister += string.Format(PacketFormat.managerRegisterFormat, packetName) + Environment.NewLine;         
이렇게 앞에서 추가해준 managerRegisterFormat라는 PacketFormat에 packetName을 전달해 코드를 생성하고 한줄 뛰어주고 managerRegiater에 넣어 줍니다. 


3. GenPackets.bat 수정
START ../../PacketGenerator/bin/PacketGenerator.exe ../../PacketGenerator/PDL.xml
XCOPY /Y GenPackets.cs "../../DummyClient/Packet"
XCOPY /Y GenPackets.cs "../../Server/Packet"
XCOPY /Y PacketManager.cs "../../DummyClient/Packet"
XCOPY /Y PacketManager.cs "../../Server/Packet"
이렇게 PacketManagers.cs도 Client, Server의 Packet폴더에 복사되게 한다. 


4. 에러 수정
4_1에러 수정1 - PacketFormat에 using 추가
생성된 PacketManagers.cs에서 에러가 발생한 부분으 잠재적 수정사항을 눌러 using을 추가한다.
PacketFormat의 managerFormat으로 가서 
public static string managerFormat =
@"using ServerCore;
using System;
using System.Collections.Generic;
이렇게 3개를 넣어준다. 
PacketGenerator 빌드하고, genPackets.bat을 실행한다. 

4_2에러 수정2 - Server의 PacketHandler 코드 안의 nameSpace Server 삭제
4_2에러 수정3 - PacketHandler 코드 안에 PlayerInfoReqHandler와 같은 인터페이스로 TestHandler추가
PDL에 패킷을 추가를 해줬으면 PacketHandler에 해당 핸들러 함수를 추가해줘야 한다. 


5. DummyClient 대칭 작업

DumyClient의 Packet 폴더에 PacketHandler 클래스 추가
Server의 PacketHandler 인터페이스를 복붙

f5를 눌러 실행을 해보면 잘 출력이 된다.


6. 패킷 분리 

패킷 분리 필요한 이유
1. PlayerInfoReqHandler는 Server의 ClientSession에만 필요한데 Client의 PacketHandler에도 필요하지 않는데 정의해 놓았다. 
2. 서버가 분산처리를 할 경우 패킷 분리를 해야 한다. 
3. 해킹 위험을 대비해 어디서 사용될 것인지, 클라에서 서버로 사용될 거인지, 서버끼리 사용될 것인지 등등을 구분을 해가지고, 안전 처리해야 한다. 
PacketManger의 Register에서 패킷을 등록을 할 때 딱 자기가 필요한 것만 Dictionary에 추가 한다.
6_1. PDL에서 구분
PDL에서 
<?xml version="1.0" encoding="utf-8" ?>
<PDL>
  <packet name ="C_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 ="S_Test">
    <int name ="testInt"/>
  </packet>
</PDL>
이렇게 C_, T_를 붙여서 패킷을 구분한다. C는 클라->서버, T는 서버->클라

6_2. PacketManager 코드 생성 자동화를 할 때 각각 필요한 코드만 생성되게 하기. 
Server쪽에 추가되는 PacketManger의 경우 Register함수에서 PacketID.PlayerInfoReq인 코드만 등록 되어야 한다. 
DummyClient쪽에 추가되는 PacketManager의 경우는 PacketID.Test인 코드만 등록이 되어야 한다. 

이를 위해 코드를 생성하는 역할을 하고 있는 PacketGenerator의 Program에 가서 PacketManager의 코드를 담고 있었던 managerRegister변수를 지우고, 
static string clientRegister, serverRegister 변수를 선언한다. 

Main에 가서 
코드를 managerFormat에 맞게 생성해 ManagerText에 넣어 PacketManager.cs에 넣어주는 코드를 
string clientManagerText = string.Format(PacketFormat.managerFormat, clientRegister);
File.WriteAllText("ClientPacketManager.cs", clientManagerText);
string serverManagerText = string.Format(PacketFormat.managerFormat, serverRegister);
File.WriteAllText("ServerPacketManager.cs", serverManagerText);
이렇게 각각의 파일로 만들게 해준다. 

ParsePacket에 가서 구분해서 패킷을 파싱해 레지스트 해주는 코드를 생성해 넣어주게 한다. 
if(packetName.StartsWith("S_") || packetName.StartsWith("s_")) 
   clientRegister += string.Format(PacketFormat.managerRegisterFormat, packetName) + Environment.NewLine;
else
   serverRegister += string.Format(PacketFormat.managerRegisterFormat, packetName) + Environment.NewLine; 

PacketGenerator를 한번 빌드 해주고, Common Packet 폴더로 가서 PacketManager를 삭제해준다. 

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"

 
GenPackets.bat 파일을 실행해서 파일이 생성되는지 확인하다. 

6_3 PacketHandler를 수정한다. 
Server의 PacketHandler로 가서 TestHandler를 삭제하고 
PlayerInfoReqHandler함수 이름을 C_PlayerInfoReqHandler로 바꾸고 PlayerInfoReq앞에 C_를 붙인다. 
DummyClient의 PacketHandler로 가서 S_TestHandler이름의 빈 함수만 남기고 PlayerInfoReqHandler함수는 삭제한다. 

솔루션 빌드를 하면 에러가 발생하는데 DummyClient의 ServerSession에 가서 PlayerInfoReq에 C_를 붙여준다. 
빌드후 실행하면 잘 출력이 된다. 

 client랑 server쪽 핸들링 하는 것도 우아하게 자동화 처리를 한 것. 


C++ 서버 다룰 때 더 최적화 하는 방법 다룰 거야. 
이 자체를 만드는게 중요한게 아니라 어떻게 돌아가는지 어떤 어려움들이 있는지를 이해하는게 가장 중요



반응형

댓글