Serialization(직렬화)

원래벌레·2022년 9월 3일
0
post-custom-banner
  • 패킷은 메모리상에 있는 녀석이었다.
    이 녀석을 네트워크상에서 흘려보내주기 위해서 byte배열에 패킷을 밀이넣어주고 있었다.

  • 그리고 센드버퍼에 위의 byte배열을 복사하여 Send를 해주는 작업을 해주었다.

  • 이렇듯 우리가 했었던 위의 작업,
    즉 메모리의 데이터를 네트워크로 흘려주기 위하여 버퍼에 밀어넣는 작업이 직렬화이다.

  • 반대로 OnRecvPacket작업처럼 버퍼에 있는 값을 가져와서 읽는 것을 역직렬화 라한다.

🌞 클라이언트쪽에서 PlayerId 요청

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

    class PlayerInfoReq : Packet
    {
        public long playerId;
    }

    class PlayerInfoOk : Packet
    {
        public int hp;
        public int attack;
    }

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

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

            PlayerInfoReq packet = new PlayerInfoReq() { packetId = (ushort)PacketID.PlayerInfoReq, playerId = 1001 };

            //for (int i = 0; i < 5; i++)
            {
                ArraySegment<byte> s = SendBufferHelper.Open(4096);

                bool success = true;
                ushort count = 0;

                //success &= BitConverter.TryWriteBytes(new Span<byte>(s.Array, s.Offset,s.Count), packet.size);
                count += 2;
                success &= BitConverter.TryWriteBytes(new Span<byte>(s.Array, s.Offset+count,s.Count-count), packet.packetId);
                count += 2;
                success &= BitConverter.TryWriteBytes(new Span<byte>(s.Array, s.Offset+count,s.Count-count), packet.playerId);
                count += 8;

                success &= BitConverter.TryWriteBytes(new Span<byte>(s.Array, s.Offset, s.Count), count);

                ArraySegment<byte> sendBuff = SendBufferHelper.Close(count);

                if(success)
                    Send(sendBuff);
            }
        }

DummyClient쪽에 Main쪽에 있던 GameSession을 클래스로 분리해주었다. 여기서의 이름은 ServerSession인데 그 이유는 대리자는 현재 서버쪽에 있기 때문에 이를 표현하기 위해서 ServerSession으로 만들어 주었다.

전 시간까지는 그냥 size와 packetId만 가지고 있는 패킷을 만들어 보내봤는데, 이번 시간에는 데이터가 있는 패킷을 보내었다.
다른점은 크게 없었다.

이런 Session을 만들때는 최대한 최적화를 해야한다.
그래서 기존에는 새로운 byte를 할당하여 해당 byte에 size, packetId, playerId를 저장했다.
이렇게 되면 안정성은 있지만, 할당과 Copy를 하는 일이 나뉘어져 있어서 속도가 느릴 것이다.
그래서 여기서 Bitconver의 TryWriteBytes메소드를 이용하여 버퍼에 바로 쓰도록 만들어 줄 수 있었다.
이렇게 최적화를 해주는 방법에는 이 방법외에도 여러가지고 있다고 한다.
TryWriteBytes 메소드의 경우 오류발생시 bool 값으로 false를 리턴하기 때문에 이를 이용하여 버퍼 이상의 값을 받는 경우를 걸러낼 수 있다.

size값은 앞에 부분임에도 마지막에 설정을 해주었다. 그 이유는 데이터를 넣으면서 크기에 해당하는 count값을 알아야 size를 넣어줄수 있기 때문이다.

이렇게해서 보내진 패킷은 Server측의 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;
                        Console.WriteLine($"PlayerInfoReq: {playerId}");
                    }
                    break;
                    
            }
            Console.WriteLine($"RecvPacketId: {id}, Size {size}");
        }

OnRecvPacket 메소드로 흘러들어간 패킷은 스위치문을 통해서 보내진 packetId를 확인하여 어떤 데이터가 들어있는지를 확인하고, 이에대한 콘텐츠 처리를 한다.


🌞 패킷의 자동 생성

  • 위의 코드에서는 Packet의 생성을 하나하나씩 해주고 있다. 하지만 이는 너무 비효율적이다. 이를 Packet 종류별 생성자를 만들어보자.

  • Packet 객체를 생성 할 때, packet의 size와 packetId를 지정하고, Write와 Read 메소드를 만들어서 패킷을 작성하거나 읽는 작업을 쉽게 만들어보자.

class PlayerInfoReq : Packet
    {
        public long playerId;
         
        public PlayerInfoReq()
        {
            this.packetId = (ushort)PacketID.PlayerInfoReq;
        }

        public override void Read(ArraySegment<byte> s)
        {
            ushort count = 0;
            //ushort size = BitConverter.ToUInt16(s.Array, s.Offset);
            count += 2;
            //ushort id = BitConverter.ToUInt16(s.Array, s.Offset + count);
            count += 2;

            this.playerId = BitConverter.ToInt64(s.Array, s.Offset + count);
            BitConverter.ToInt64(new ReadOnlySpan<byte>(s.Array, s.Offset + count, s.Count - count));
            count += 8;
        }

        public override ArraySegment<byte> Write()
        {
            ArraySegment<byte> s = SendBufferHelper.Open(4096);

            bool success = true;
            ushort count = 0;

            //success &= BitConverter.TryWriteBytes(new Span<byte>(s.Array, s.Offset,s.Count), packet.size);
            count += 2;
            success &= BitConverter.TryWriteBytes(new Span<byte>(s.Array, s.Offset+count,s.Count-count), this.packetId);
            count += 2;
            success &= BitConverter.TryWriteBytes(new Span<byte>(s.Array, s.Offset+count,s.Count-count), this.playerId);
            count += 8;
            success &= BitConverter.TryWriteBytes(new Span<byte>(s.Array, s.Offset, s.Count), count);

            if( success == false )
                return null;

            return SendBufferHelper.Close(count);
        }
    }
  • 위에서 눈여겨 봐야할 곳은 Read 메소드이다. ArraySegment를 이용하여 배열의 유효한 값이 있는 지점을 count를 통하여 조정하고 있다. 그렇지만 여기서 패킷이 조작되어 기존의 ushort, ushort, long 값이 추가되어 2, 2, 8 로 count를 늘려주던 Array의 Count를 2, 2, 4로 count를 늘려준다면 분명 잘못된 count수를 넣어줬음에도 이 패킷을 받는 서버는 이를 잡지 못하고, 2, 2, 8을 넣어줬을 때와 같은 결과를 반환한다. 그러기 위해서 넣은 것이 BitConverter.ToInt64(ReadOnlySpan ~ ) 이다. 이 ToInt64는 여러가지 버전이 있다. 그 중에서도 이 버전은 Span을 생성함으로써 만약에 유효한 범위 밖으로 값이 작성이 된다면 Exception을 일으킨다.
profile
학습한 내용을 담은 블로그 입니다.
post-custom-banner

0개의 댓글