Server programming

03_10_패킷 직렬화_PacketGenerator #5_ switch문의 비효율성 개선, case 파싱 자동화

devRiripong 2023. 4. 20.
반응형

다룰 내용 및 요약

프로토콜이 많이 늘어날 경우 switch문의 비효율성을 개선하고 case문을 만들고 파싱하는 것을 자동화하고자 합니다. 다음과 같은 단계를 진행합니다.

프로토콜이 많이 늘어날 경우 switch문의 비효율성을 개선하고 case문을 만들고 파싱하는 것을 자동화하고자 합니다. 다음과 같은 단계를 진행합니다.

  1. IPacket 인터페이스를 정의하고 구현합니다.
  2. 자동화를 위해 PacketFormat을 수정하고, .bat 파일로 갱신합니다.
  3. ClientSession의 OnRecvPacket 코드를 자동화합니다.

    3-1) 코드 이주

  • PacketHandler와 PacketManager 클래스를 생성하고 ClientSession의 OnRecvPacket에서 받은 데이터를 사용하는 코드를 옮겨줍니다.
  • PacketManager에서 OnRecvPacket 함수를 선언하고 ClientSession의 OnRecvPacket 나머지 코드를 옮겨줍니다.

   3-2) 자동화

  • Dictionary를 사용하여 자동화를 구현합니다.
  • Register 함수를 만들어 프로토콜 ID와 작업을 등록합니다.
  • MakePacket 함수를 생성하여 패킷을 만들고 버퍼를 읽은 다음, handler에서 프로토콜을 사용하여 작업을 호출합니다.

마지막으로, Server의 Program.cs에서 Main 시작 부분에서 PacketManager.Instance.Register()를 호출하게 합니다. 이렇게 하면, 멀티스레드에 영향을 받지 않고, 프로토콜이 많아져도 효율적으로 처리할 수 있으며, case문 생성 및 파싱이 자동화됩니다. 단, PacketHandler는 수동으로 추가해야 하지만, PacketManager는 PacketGenerator에서 자동으로 생성할 수 있습니다.

 

 

 

 

여기까지 해서 프로젝트를 그냥 진행해도 무리가 없긴 한데,

굳이 개선점을 찾아보자면, 패킷을 받은 다음에 어떤 행동을 하고 있는지 살펴 보자.

ClientSessionOnRecvPacket에서

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

패킷을 받아서 조립을 한 다음에

패킷으로 뭔가를 하는 작업switch문 안에 넣어 놨었어.

프로젝트에 따라 그냥 이렇게 하는 경우도 있다.

게임 규모가 커지게 되면 프로토콜에 패킷이 엄청 많이 들어가게 될 것이다. 그러면 PlayerInfoReq가 몇 백개가 될 수도 있는데 그 말은 switch문 안에 case가 200개씩 들어가게 된다는 말이다.

그러면 비효율적일 거야. 만약 case가 200개 있는데 199번째 case에서 걸리는 패킷을 조립해야 한다고 치면, 198번은 쓸 데 없는 case문 비교를 해야 하는 거니까 이 부분을 개선하면 좋겠다는 생각이 든다.

패킷은 굉장히 자주 호출되고, 클라이언트랑 서버랑 통신할 때 당연히 패킷을 이용할 테니까, 이 부분을 개선할 수 있으면 상당히 큰 이득으로 돌아오게 될 거다.

성능적 개선 외에도 이런식으로 직접 case문을 만들어 준 다음에 파싱하는 부분을 직접 넣는 것도 상당히 귀찮은 작업 이니까 이런 것들도 자동화 하는 방법에 대해서 추가로 알아보도록 할거야.

1. IPacket 인터페이스 정의, 상속받아 인터페이스 구현

일단은 GenPacket.cs에다가 뭔가를 좀 추가해보도록 할거야.

class PlayerInfoReq 
{

일단은 이런식으로 패킷을 클래스로 갖고 있긴 했는데

나중에 이 switch case문을 한땀 한땀 만드는게 아니라 자동화 하는 코드를 만들고자 하면 이왕이면 얘가 어떤 IPacket이라는 인터페이스를 상속 받아서 공용으로 사용을 하면 조금 편하게 만들 수 있다. 나중에 인자로 IPacket이란 인터페이스를 함수끼리 넘겨가지고, 모든 패킷에 대해서 공통 인수로 넘길 수가 있기 때문에 이걸 인터페이스로 맞춰 보도록 하자.

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

모든 패킷이 인터페이스를 구현하는 방식으로 작업을 할 거

반드시 IPacket이라는 애는 이 세가지를 다 포함하고 있어야 한다는 얘기가 되는 거고,

class PlayerInfoReq에다가 인터페이스를 구현 할 때에는 (잠재적 수정사항에서 인터페이스 구현을 누른다)

Protocol을 어떻게 구현하냐면, Write에 Protocol을 사용하는 부분이 있었어.

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

나중에 자동화 할 때 여기있는 애들이 필요 하니까 사용하고 있는 거.

2. 자동화 위해 PacketFormat 추가

이거를 자동화 하는 코드로 맞춰 주려면 PacketFormat.cs에 돌아가서 아까 추가한 부분만 넣어주면 되는데

GenPacket의

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

이 부분을 복사를 해서 PacketFormat클래스의 fileFormat으로 가서 추가를 해주면 된다.

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

이즈음에 넣어주면 될거고,

까먹지 말고 소괄호를 2개로 해준다.

PlayerInfoReq의 IPacket의 구현부

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

Protocol 코드

PacketFormat.cs에 간 다음에

public static string packetFormat =에 위치를 할 텐데

public static string packetFormat =
@"
class {0} 
{{
    {1}     

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

    public void Read(ArraySegment<byte> segment)
    {{

Read 위에 이런 식으로 위치시켜 주자.

얘도 중괄호를 추가해준다. 그리고 PlayerInfoReq도 {0}으로 바꿔준다.

그리고 IPacket을 상속 받게 해준다.

public static string packetFormat =
@"
class {0} : IPacket
{{
    {1}     

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

이 상태에서 PacketGenerator를 다시 빌드를 해줘서 실행파일을 갱신 해주면 이제 전 시간에 만들었던 .bat 파일만 갱신을 해주면 된다. GenPackets.bat파일을 실행해 준다.

그리고 GenPackets.cs를 보면

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

class PlayerInfoReq : IPacket
{
public ushort Protocol { get { return (ushort)PacketID.PlayerInfoReq; } }

이렇게 변동된 버전으로 뜬걸 볼 수 있다.

가끔 갱신이 안될 때가 있는데 지운 다음에 .bat 파일을 실행하면 정상적으로 바뀔 것이다.

프로토콜도 잘 추가되어 있는걸 볼 수 있다.

3. ClientSession의 OnRecvPacket코드 자동화

3-1)코드 이동

이어서 작업을 해보자

ClientSessionOnRecvPacket안의 부분을 자동화 하는 코드로 넣어주고 싶다.

준비할 것이 있다.

Server의 Packet 폴더에다가 새 항목 추가를 누르고 클래스, 이름은 하나는 PacketHandler로 하고,

하나는 PacketManager라고 한다.

PacketHandler로 가서 namespace에서 Server.Packet에서 .Packet은 지워버린다. PacketManager도 마찬가지로 해준다.

PacketHandler 같은 경우에는 어떤 역할을 하는 거냐면, 얘는 우리가 수동으로 관리를 해줄거고, 여기다가 말그대로 해당 패킷이 다 조립이 됐으면 무엇을 호출할까를 맞춰 주도록 할거야.

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

        }

어떤 세션에서 조립이 되었느냐, 어떤 패킷이냐를 이렇게 받아 온 거.

그리고 받은 패킷을 사용할 때는 playerInfoReq로 캐스팅을 한번 해줘야 한다.

PlayerInfoReq p = packet as PlayerInfoReq;

그 다음에 우리가 이전 시간에 ClientSession에서 넣었던 부분, 사실은

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

여기 있는 부분이다. 여기 코드를 ctrl+x로 자른 다음에

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

PacketHandler에다가 이렇게 이동을 시켜줘서

여기서 이제 실제로 어떤 일을 할지를 넣어주게 된다.

여기있는 코드와 함수도 직접 만들어 줘야 하지만 PlayerInfoReqHandler를 호출하는 부분을 통째로 자동화 하게 될거야.

ClientSession의 OnRecvPacket에서 Handler하는 부분(핸들러는 패킷을 받아서 그에 따른 적절한 작업을 수행하는 코드 부분)은 날렸고, 그 다음엔

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:
            {
                PlayerInfoReq p = new PlayerInfoReq();
                p.Read(buffer);                
            }
            break;
    }**
    Console.WriteLine($"RecvPacketId: {id}, Size: {size}");
}

여기있는 부분을 추가로 자동화 코드에 넣어줘야 하는데

그것을 어디서 해야 할거냐 하면

PacketManager에다가 여기서 만들어 주도록 할거야.

참고로 원하는대로 만들어도 된다. 굳이 딱히 어떤 방법이 있는 건 아닌데

PacketHandler를 다 등록을 한 다음에는 딱히 수정을 할 것은 없기 때문에 싱글톤으로 만들어 주도록 할거야.

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

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

    }
}

Singleton에 대한 내용은 이전에 유니티를 다룰 때도 언급한 적이 있었는데 단순하면서도 편리하다

PacketManager를 사용하고 싶을 때는 PacketManager.Instance를 사용하게 되면 여기서 만들어준 static에 들어가 있는 변수 _instance를 바로 반환해서 마치 PacketManager가 전체 코드에서 하나만 있는 거처럼 사용할 수 있게 된다.

그렇다고 얘를 너무 남발하면 안되고, 정말로 하나로 사용할 것을 이렇게 만들어 주면 된다.

 

그 다음에 PacketManager에

public void OnRecvPacket(PacketSession session, ArraySegment<byte> buffer)
        {

        }

이게 필요한데

ClientSession의

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:
            {
                PlayerInfoReq p = new PlayerInfoReq();
                p.Read(buffer);                
            }
            break;
    }**
    Console.WriteLine($"RecvPacketId: {id}, Size: {size}");
}

이 부분이 이사를 해야 한다. 로그는 안찍어도 되니까 삭제를 하고,

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

        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;

            switch ((PacketID)id)
            {
                case PacketID.PlayerInfoReq:
                    {
                        PlayerInfoReq p = new PlayerInfoReq();
                        p.Read(buffer);
                    }
                    break;
            }
        }

    }
}

이사를 하고

OnRecvPacket에서는

public override void OnRecvPacket(ArraySegment<byte> buffer)
        {
            PacketManager.Instance.OnRecvPacket(this, buffer);
        }

이렇게 연결 시켜 주는 작업을 해준다.

물론 이렇게 싱글톤으로 굳이 접근하는게 아니라

PacketManager.Instance이거 자체를 우리가 기존에 사용하던 Handler방식으로 등록을 해줘가지고 걔를 호출하는 것도 상관은 없지만, 일단은 이렇게 싱글톤을 막바로 호출하는 방식으로 구현해본다.

그럼 OnRecvPacket이 다 PacketManager안에 들어가게 된 거다.

3-2) 자동화

이제 이어서 작업을 하면 되는데 사실 이렇게 옮겼다고 해서 자동화라고 볼 수는 없어.

이제 부터 자동화를할 건데

PacketManager의 switch case문을 이런식으로 하나하나 하는게 아니라 자동으로 등록하는 시스템을 만들어 주도록 하자.

일단은 Dictionary를 만들어 주도록할거야.

그리고 switch case라는 건

case PacketID.PlayerInfoReq:

여기있는 프로토콜 아이디가 있을거고, 그 프로토콜 아이디에 따라서

{
    PlayerInfoReq p = new PlayerInfoReq();
    p.Read(buffer);
}

어떤 패킷을 만들어 주고,패킷의 어떤 함수를 호출 하라는 그런 일련의 과정들을 연결 시켜주는 작업이기 때문에 결국 Dictionary를 만들건데

처음에는 프로토콜 아이디로 구별을 할거고, 어떤 행동을 할 것이냐두번째로 정의를 할건데

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

_onRecv를 할 때 Dictionary<ushort, Action<PacketSession, ArraySegment<byte>>> 프로토콜 id랑 어떤 작업을 할지 등록을 해줘야 하니까, Register라는 기능을 만들어 줘가지고, 모든것을 등록하는 작업을 해보자.

이 부분이 PacketGenerator에서 했던 거 처럼 자동화 하면 될거야. 일단 _onRecv에 뭐가 들어가야 할지 직접 손으로 만들어 본 다음에 걔를 자동화 하는 작업을 할거야.

public void Register()
{
    _onRecv.Add((ushort)PacketID.PlayerInfoReq, )
}

이 프로토콜 아이디에 해당하는 애가 무엇을 할지두번째 인자로 넘겨줘야 하는데 얘를 아직 정의를 하지 않았지만, 대충 인터페이스는

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

이런 식으로 연동을 해주도록 할거야.

그럼 이 MakePacket이라는 애를 이어서 만들어 주면 된다.

void MakePacket<T>

이렇게 제네릭 타입으로 만들어 줬는데 그래야 PlayerInfoReq든 패킷 클래스를 넘겨줄 수 있을거야.

void MakePacket<T>(PacketSession session, ArraySegment<byte> buffer) where T : IPacket, new()

제네릭 T는 아무거나 다 되는 건 아니고, IPacket을 인터페이스로 구현하는 클래스여야 되고, new()가 가능해야 되는 애라고 조건을 추가를 해주도록 할거야.

그럼 여기서 하는 일은 우리가 switch case문에서 무얼 했냐면 만약 ID가 PlayerInfoReq였으면

PlayerInfoReq p = new PlayerInfoReq();
p.Read(buffer);

얘를 하고 있었어, 결국 얘를 MakePacket에 넣어준다.

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

근데 PlayerInfoReq라는 보장은 없으니까, PlayerInfoReq가 T라는 애로 치환을 해주면 된다.

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

그럼 나중에 자동화 할 때는

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

PlayerInfoReq부분을 계속 인자를 바꿔 주면 다른 경우에도 다 처리가 될 거다.

그럼 MakePacket에서 했던 거는 switch case문 안의 패킷을 만들어 주는 작업을 하고 있었던 거고,

패킷을 만들어 준 다음에는 새로 PacketHandler라고 만들어준 이 인터페이스 쪽으로 넘겨줘야 하는데 걔를 또 연동을 시켜 줘야 한다.

근데 PlayerInfoHandler를 호출해줄 방법은 딱히 아직 없으니까 고민하다 PacketManager에 하나 더 만들어 주도록 할거야.

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

Action은 말 그대로 public static void PlayerInfoReqHandler(PacketSession session, IPacket packet) 정의한 이 형식대로의 액션을 Delegate를 정의해준 것이다.

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

이제 Register를 할 때 첫번째패킷을 만들어 주는 것을 등록을 하는거였고,

두번째 같은 경우는 똑같긴 한데 얘같은 경우에는 진짜로 PacketHander.PlayerInfoReqHandler로 넘겨 달라 부탁을 하는 거 .

PlayerInfoReqHandler 여기 있는 이름은 우리가 규약을 하나 만들었었어. 패킷이름에다 Handler를 붙인 함수를 호출하면 됩니다. 라고 요청을 하는 거고,

그 함수는 PacketHandler.cs에다가 준비를 해주면 된다.

이렇게 연속해서 만들고 있는게 지금 봤을 땐 헷갈리고 이해 안갈 수 있는데 얘를 한번 다 조립을 하고 난 다음에 보면 이해 하기 쉬울 거야.

결국 PacketManager에서 MakePacket을 해줘가지고, OnRecvPacket의 switch case 부분에서 패킷을 만들어 준 다음에 핸들러를 호출하는 거니까, 그 부분을 해주도록 할거야.

MakePacket에서 Action을 받아 올건PacketSession에다가 IPacket이라는 두번째 인자를 받아주는 액션을 찾아 올거야.

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

_handler에다 넣어 놓은 것 중에서 TryGetValue로 여기에 우리가 방금 만든 packet의 Protocol을 참조 해가지고, 그 액션 out action 우리가 어떤 행동을 해야 할지를 추출 해오는 거다.

만약에 그 Protocol이 존재 하는 프로토콜 아이디여서 액션을 뽑아 왔으면 TryGetValue가 true를 리턴할 테니까 Invoke를 해서 sessionpacket을 이렇게 넘겨주면 된다.

p가 성의 없으니 pck로 바꾸도록 하자.

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

그래서 MakePacket을 호출을 하면 실제로 패킷을 만들어 준 다음에 핸들러를 호출해 주는 작업까지 한거고, 그렇다는 건 OnRecvPacket을 했을 때

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;

            switch ((PacketID)id)
            {
                case PacketID.PlayerInfoReq:
                    {
                        PlayerInfoReq p = new PlayerInfoReq();
                        p.Read(buffer);
                    }
                    break;
            }
        }

이런 식으로 switch case문을 사용해서 만드는 게 아니라

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

여기 _onRecv에다가

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

여기있는 어떤 액션 인지를 찾아 가지고, 호출을 해주면 된다.

즉 _onRecv에는 MakerPacket이라는 애가 등록이 되어 있으니까 어떤 애인지는

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;
//액션을 일단 찾을 건데 PacketSession과 ArraySegment<byte>를 받는 Delegate이고 
    Action<PacketSession, ArraySegment<byte>> action = null; 
// _onRecv에서 TryGetValue를 하는데 id로 찾아 올거고,그거에 따른 action을 받아 올건데
    if (_onRecv.TryGetValue(id, out action))
// 만약 그 action이 실제로 있다고 하면 action.Invoke를 해서 session과 buffer를 각각 넣어주면 된다. 
        action.Invoke(session, buffer); 
    //switch ((PacketID)id)
    //{
    //    case PacketID.PlayerInfoReq:
    //        {
    //            PlayerInfoReq p = new PlayerInfoReq();
    //            p.Read(buffer);
    //        }
    //        break;
    //}
}

결국 다시 한번 흐름을 보면

ClientSession의 기존의 OnRecvPacket이란 거에서 PacketManager에다가 넘겨 준거,

PacketManager의 OnRecvPacket에서는 처음에 sizeid를 긁어 와서 id에 따라 가지고 switch case문이 하던 거를 이제는 조금 더 빠르게 Dictinonary에서 찾아 온 다음에 실제로 우리가 핸들러를 등록해 놨다면 걔를 Invoke를 할 것이다. 걔를 호출하는 순간 MakePacket 부분이 호출 될거야. 왜냐하면 Register에다가 등록을 할 때 MakePacket이라는 애를 PlayerInfoReq라는 애로 등록을 시켜 놨으니까, MakePacket에서는 pktPlayerInfoReq라는 애가 만들어 지면서, Read를 해서 DeSerialize를 한 다음에 걔와 연관된 이벤트 핸들러를 호출을 하면서 최종적으로는 PacketHandler.cs의 PacketHandler에 등록한 PlayerInfoReqHandler코드가 호출이 될거야.

 

이제 얘를 한번 테스트를 해봐야 하는데 하기 앞서서 PacketManager.cs의 Register라는 부분은 언제 한번 해주긴 해줘야 한다. 그리고 multi thread가 개입 되기 전에 맨 처음에 우선적으로 해주도록 하자.

Server의 Program.cs에 가가지고, 멀티스레드가 개입을 하지 않는 부분 Main 시작하는 부분에 해주도록 한다.

static void Main(string[] args)
{
    PacketManager.Instance.Register();

이걸 처음에 해주도록 한다.

여기까지는 싱글 스레드로 돌아가고 있었으니까

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

여기 Register 코드도 아무 문제 없이 할 수 있을 거야.

그게 아니라 실제로 패킷을 PacketManager의 OnRecvPacket에서 받는 도중에 그 상태에서 Register를 해버리면 조금 문제가 있을 거야.

 

왜냐하면

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

Dictionary에 처리를 안하고 그냥 하게 되는 것이기 때문이다.

근데 사실 처음에만 등록을 잘 해 놨다고 가정하면 그 다음에는 변환하면서 _onRecv나 _handler에 데이터를 추가하면서 동작하는게 아니라 단순히 조회만 하는 거다 실제로 action이 있는지. 그러니까 나머지 부분은 멀티스레드 환경에서도 딱히 문제가 되지 않는 코드들이 된다.

그럼 여기까지 실행을 해보기 위해 솔루션 속성에 가서 여러개 시작 프로젝트로 바꿔 놓은 다음에 실행을 해보자.

스킬도 파싱이 잘 되고 있다는 걸 볼 수 있다.

 

class PacketHandler는 직접 수동으로 추가로 해줘야 하는 건 어쩔 수 없지만, .

나머지 부분은 PacketManager도 PacketGenerator에서 자동으로 생성을 해주면 PDL에 새로운 걸 추가를 하더라도 굳이 걔를 파싱하고 new하는 부분을 신경쓸 게 아니라 PacketHandler에다가 얘를 처리하는 인터페이스 함수 하나만 파주면 된다는 얘기다.

 

 

작업한 코드

Server의 Packet폴더의 PacketHandler.cs

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

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

Server의 Packet폴더의 PacketManager.cs

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

namespace Server
{
    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()
        {
            _onRecv.Add((ushort)PacketID.PlayerInfoReq, MakePacket<PlayerInfoReq>); 
            _handler.Add((ushort)PacketID.PlayerInfoReq, PacketHandler.PlayerInfoReqHandler); 
        }

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

Server의 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
{
	class ClientSession : PacketSession
    {
        public override void OnConnected(EndPoint endPoint)
        {
            Console.WriteLine($"OnConnected : {endPoint}");

            Thread.Sleep(5000);
            Disconnect();
        }
        public override void OnRecvPacket(ArraySegment<byte> buffer)
        {
            PacketManager.Instance.OnRecvPacket(this, buffer);
        }

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

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

}

Server의 Program.cs

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

namespace Server
{  
    class Program
    {
        static Listener _listener = new Listener();

        static void Main(string[] args)
        {
            PacketManager.Instance.Register();

            // DNS ( Domain Name System )
            string host = Dns.GetHostName();
            IPHostEntry ipHost = Dns.GetHostEntry(host);
            IPAddress ipAddr = ipHost.AddressList[0];
            IPEndPoint endPoint = new IPEndPoint(ipAddr, 7777);
            // ipAddre는 식당 주소, 7777은 식당 정문인지 후문인지 문의 번호

            _listener.Init(endPoint, () => { return new ClientSession(); });
            Console.WriteLine("Listening...");

            while (true)
            {
                ;
            }
        }
    }
}

PacketGenerator의 PacketFormat.cs

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

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

GenPackets.cs

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

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

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

class PlayerInfoReq : IPacket
{
    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 ushort Protocol { get { return (ushort)PacketID.PlayerInfoReq; } }

    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 Test : IPacket
{
    public int testInt;     

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

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

확인 테스트

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


1. IPacket 인터페이스 정의, 상속 받아서 인터페이스 구현
-> GenPacket.cs로 가서 IPacket 인터페이스를 정의한다. 
interface IPacket
{
ushort Protocol { get; }
void Read(ArraySegment<byte> segment);
ArraySegment<byte> Write();
}

PlayerInfoReq : IPacket 이렇게 상속받게 해주고
PlayerInfoReq클래스로 가서 
public ushort Protocol { get { return (ushort)PacketID.PlayerInfoReq; } }
이렇게 구현해 준다. 


2. 자동화 위해 PacketFormat 추가, .bat 파일로 갱신
-> 자동화 하는 코드로 해주기 위해 PacketFormat.cs로 가서 fileFormat에 
interface IPacket을 추가해 주고, 중괄호도 2개로 해준다. 

Protocol의 구현부도 packetFormat의 Read위에 추가해주고, 중괄호도 2개로 해준다. PlayerInfoReq도 {0}으로 바꿔준다. 
packetFormat의 class {0}가 Ipacket을 상속받게 한다. 

PacketGenerator를 빌드 해주고, GenPacekts.bat파일을 실행한다. 
GenPackets.cs를 보면 IPacket이 들어가 있고, PlayerInfoReq가 IPacket을 상속받고 있으며, Protocol도 구현되어 있는 것을 볼 수 있다. 

3. ClientSession의 OnRecvPacket코드 자동화
3-1) 코드 이주
-> ClientSession의 OnRecvPacket안의 부분을 자동화 하는 코드로 넣어주고 싶다.
준비해야 할 것이 있다. 
Server의 Packet폴더에 PacketHandler와 PacketManager라는 클래스 파일을 만들어 준다. 그리고 각 namespace에서 .Packet은 삭제한다. 

PacketHandler는 수동으로 관리 해줄 거고, 패킷이 조립 됐으면 무엇을 호출할까를 써줄 거야. 
PacketSession과 IPacket을 받는 PlayerInfoReqHandler함수를 선언한다. 
PlayerInfoReq p = packet as PlayerInfoReq; 이렇게 매개변수로 받은 packet을 캐스팅 해서 p에 넣어준다. 
그리고 ClientSession의 OnRecvPacket에서 case PacketID.PlayerInfoReq:의 
Console.WriteLine($"PlayerInfoReq: {p.playerId} {p.name}");
foreach (PlayerInfoReq.Skill skill in p.skills)
{
Console.WriteLine($"Skill({skill.id})({skill.level})({skill.duration})");
}
이 부분도 PacketHandler의 PlayerInfoReqHandler함수로 옮겨준다.

ClientSession의 OnRecvPacket의 핸들러 하는 부분을 제외한 코드는 자동화 코드에 넣어줘야 한다. 
PacketManager에다가 만들어 줘야 한다. 
PacketManager _instance를 return하는 싱글톤을 구현해 주고, 
PacketSession과 ArraySegment를 받는 OnRecvPacket 함수를 선언 하고 ClientSession의 OnRecvPacket의 나머지 코드를 옮겨준다. 
ClientSession의 OnRecvPacket은 PacketManager.Instance.OnRecvPacket(this, buffer)를 호출하게 한다. 
아직 옮기기만 한 거 

3-2) 자동화 
자동화를 해준다. 
 switch case문이 아니라 자동화 하기 위해 Dictionary를 만들어 준다. 
프로토콜 id랑 어떤 작업을 할지 등록하는 용으로 PacketManager.cs에 
Dictionary<ushort, Action<PacketSession, ArraySegment<byte>>> _onRecv = new Dictionary<ushort, Action<PacketSession, ArraySegment<byte>>>();
를 생성한다. 
그리고 Register라는 함수를 만들어 이를 사용하자. 
public void Register()
{
    _onRecv.Add((ushort)PacketID.PlayerInfoReq, MakePacket<PlayerInfoReq>); 
}

무엇을 해줄지를 뜻하는 MakePacket을 만들어 준다. 
제네릭 타입으로 만들어 주고, IPacket인터페이스로 구현된 클래스이고, new가 가능해야 한다는 조건을 주고, switch case문에서 해줬던 것을 넣어 준다. 
void MakePacket<T>(PacketSession session, ArraySegment<byte> buffer) where T : IPacket, new()
{
    T p = new T();
    p.Read(buffer);
}

switch case문에서 패킷을 만들어 준 다음 그 다음에 있던 코드를 옮긴 곳인 PacketHandler의 PlayerInfoHandler를 호출해 줘야 한다. 
호출해 줄 방법으로 PacketManager에 Dictionary를 하나 더 만들어 준다. 
public static void PlayerInfoReqHandler(PacketSession session, IPacket packet)의 형식을 반영하여
Dictionary<ushort, Action<PacketSession, IPacket>> _handler = new Dictionary<ushort, Action<PacketSession, IPacket>>();
이렇게 만들어 주고 Register에서
public void Register()
{
    _onRecv.Add((ushort)PacketID.PlayerInfoReq, MakePacket<PlayerInfoReq>); 
    _handler.Add((ushort)PacketID.PlayerInfoReq, PacketHandler.PlayerInfoReqHandler); 
}
이렇게 추가해 준다. 

MakePacket에서 패킷을 만들고 buffer를 읽은 다음에 _handler에서 Protocol로 action을 뽑은 다음에 session과 패킷을 전달해 함수를 호출한다. 
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); 
}

packetManager에서 OnRecvPacket을 했을 때
action을 선언하고, id를 buffer로 부터 추출해서 _onRecv에서 TryGetVlue로 action을 받고, action을 받아 왔다면 session과 buffer를 넣어 Invoke하면 된다.
그러면 바로 위에서 말한 MakePacket이 실행 될 것이고, Packet을 생성하고, _handler에서 protocol로 action을 찾아 session과 pkt을 넣어 invoke해준다. 
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); 
}


Register가 멀티 스레드가 개입 되기 전에 실행 되어야 하기 때문에 
Server의 Program.cs
static void Main(string[] args)
{
    PacketManager.Instance.Register();
여기서 해주도록 한다. 


솔루션 속성을 여러개 시작 프로젝트로 바꿔서 실행을 해보자. 

PacketHandler는 수동으로 추가해 줘야 하지만, PacketManager는 PacketGenerator에서 자동으로 생성할 수 있다. 

반응형

댓글