Skip to content

Latest commit

 

History

History
306 lines (231 loc) · 11.9 KB

File metadata and controls

306 lines (231 loc) · 11.9 KB

직렬화 하는 과정을 자동화하는 데 있어서 계속 테스트하면서 어떤 인터페이스로 제공을 할지

끊임없이 고민을 해보아야 한다.

Packet.Read와 Packet.Write의 기준

Packet.Write

클라이언트에 있는 서버 세션에서 서버와 연결이 되었을 때 패킷의 정보를 보내는 때 이때 보내야할 패킷의 정보를 쓰고(== byte array로 직렬화) 해당 패킷의 정보를 send 한다.

Packet.Read

서버에 있는 클라이언트 세션에서 클라이언트가 보낸 패킷을 받았을 때 byte array로 보낸 패킷을 읽어서 패킷 아이디에 따라 패킷의 정보를 읽고(== 역직렬화) 새롭게 생성한 해당 패킷의 객체에 할당하는 과정이다.

문제점

서버에서 받은 패킷의 실제 사이즈와 패킷을 까서 획득한 실제 사이즈 정보를 비교할 때 서로 다를 수가 있다.

서버를 만들 때는 클라는 항상 거짓말을 하고 있다고 가정한 상태에서 만들어야 한다.

예를 들어 패킷을 보낼 때

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

위와 같이 실제는 12 바이트지만 4바이트라고 보냈다고 가정을 하면

해당 패킷 정보를 수령한 서버 쪽에서는 아무런 문제 없이 정상적으로 동작을 한다 (단지 패킷의 사이즈만 잘못된 값으로 들어갈 뿐이다).

이와 같은 일이 왜 일어나면

실제로 받은 패킷의 정보를 처리하는 RecvBuffer에서 패킷의 정보를 까서 획득한 사이즈로 유효 범위를 지정을 하는 데

실제로 해당 유효 범위를 벗어나더라도 RecvBuffer 크기는 여유가 있기 때문에 지정한 유효 범위를 벗어나서 값을 할당하게 된다.

따라서 Read를 할 때 받은 byte array의 사이즈를 확인 안하고 바로 BitConverter로 값을 역직렬화 하면 강제로 값을 가지고 올 수 있게 되는 것이다.

실습 예제

Packet

// 패킷으로 보내기 위해서는 사이즈를 최대한 압축하는 것이 좋다.
// 스노우 볼 효과가 나타날 수 있음
using System;
using ServerCore;

public abstract class Packet
{
        public ushort size; // 2
        public ushort packetId; // 2
        public abstract ArraySegment<byte> Write();
        public abstract void Read(ArraySegment<byte> s);
}

class PlayerInfoReq : Packet
{
    public long playerId; // 8
    public PlayerInfoReq()
    {
        // PlayerInfoReq의 PacketID는 이미 정해져 있으므로 생성자를 통해 초기화
        this.packetId = (ushort)PacketID.PlayerInfoReq;
    }

    // Read는 받은 패킷을 하나씩 까보면서 값을 할당하고 있기 때문에 
    // 조심해야할 필요가 있다.
    public override void Read(ArraySegment<byte> s)
    {
        ushort count = 0;
        // ushort size = BitConverter.ToUInt16(s.Array, s.Offset);
        count += 2;
        // ushort packetId = BitConverter.ToUInt16(s.Array, s.Offset + count);
        count += 2;
        
        // 전달 받은 패킷에 있던 playerId를 자신의 playerId에 할당
        // ReadOnlySpan
        // byte array에서 내가 원하는 범위에 있는 값만 읽어서 역직렬화 한다.
        // s.Array : 대상 byte array
        // s.Offset + count : 시작 위치
        // s.Count - count : 원하는 범위 => 전달 받은 byte array 크기 - 앞에서 빼는 패킷 데이터 크기 
        // => 실제로 읽어야할 byte array 크기
        // byte array의 크기가 실제로 읽어야할 byte array 크기와 다르면 Exception을 뱉어 낸다.
        // 위 Exception을 통해 패킷 조작을 의심할 수 있고 바로 Disconnect 시킬 수 있다.
        this.playerId = BitConverter.ToInt64(new ReadOnlySpan<byte>(s.Array, s.Offset + count, s.Count - count));        
        count += 8;
    }

    // SendBuffer를 통해 보낼 패킷의 정보를 하나의 ArraySegment에 밀어 넣은 다음에 해당 값을 반환
    // write의 경우 패킷에 원하는 값은 넣는 모든 과정을 직접 컨트롤 하고 있기 때문에 별 문제가 없다.
    public override ArraySegment<byte> Write()
    {
        ArraySegment<byte> s = SendBufferHelper.Open(4096);
        ushort count = 0;
        bool success = true;

        // 클라로 패킷을 전달하기 전에 ArraySegment에 패킷의 정보를 밀어 넣는 작업
        // success &= BitConverter.TryWriteBytes(new Span<byte>(s.Array, s.Offset, s.Count), this.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);
    }
}

// 패킷 아이디로 패킷을 구분
// 나중에는 자동화를 할 예정
public enum PacketID
{
    PlayerInfoReq = 1,
    PlayerInfoOk = 2,
}

Server.ClientSession

using System;
using System.Net;
using System.Threading;
using ServerCore;

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

            // Packet packet = new Packet(){ size = 4, packetId = 7 };
            // 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> sendBuffer = SendBufferHelper.Close(packet.size);

            // Send(sendBuffer);
            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 packetId = BitConverter.ToUInt16(buffer.Array, buffer.Offset + count);
            count += 2;

            switch((PacketID)packetId)
            {
                case PacketID.PlayerInfoReq:
                    {
                        PlayerInfoReq p = new PlayerInfoReq();
                        p.Read(buffer);
                        count += 8;
                        System.Console.WriteLine($"PlayerInfoReq : {p.playerId}");
                    }
                    break;
            }
            System.Console.WriteLine($"Recive Package size : {size}, ID : {packetId}");
        }

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

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

DummyClient.ServerSession

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

        PlayerInfoReq packet = new PlayerInfoReq(){ playerId = 1001 };

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

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

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

    public override int OnRecv(ArraySegment<byte> buffer)
    {
        string recvData = Encoding.UTF8.GetString(buffer.Array, buffer.Offset, buffer.Count);
        System.Console.WriteLine($"From Server : {recvData}");
        // 일반적인 경우라면 모든 데이터를 처리 했기 때문에 전체 갯수를 처리했다고 알림
        return buffer.Count;
    }

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

의문점

// PlayerInfoReq : Packet
public override ArraySegment<byte> Write()
{
    ArraySegment<byte> voz = SendBufferHelper.Open(4096);
    ushort count = 0;
    bool success = true;

    // 클라로 패킷을 전달하기 전에 ArraySegment에 패킷의 정보를 밀어 넣는 작업
    // success &= BitConverter.TryWriteBytes(new Span<byte>(s.Array, s.Offset, s.Count), this.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);
}

위 코드에서 Close만 했을 뿐인데 어떻게 ArraySegment<byte> s 에 쌓인 버퍼를 전달하는 걸까?

SendBufferHelper를 통해 SendBuffer 생성되는 로직에 대한 이해가 필요하다.

우선 SendBufferHelper.Open()을 호출하면 ThreadLocalStorage에 SendBuffer가 저장이 되고 SendBufferHelper는 앞으로 자신의 쓰레드에서 들고 있는 ThreadLocalStorage에 있는 SendBuffer에 접근을 해서 값을 가지고 오게 된다.

그렇게 Open()을 한 다음에 이후 TryWriteBytes를 통해 s의 값을 바꿔주고

ArraySegment<byte> s = SendBufferHelper.Open(4096);

SendBufferHelper를 통해 Open만 해줬을 뿐인데 어떻게 s가 ThreadLocalStorage에 있는 SendBuffer의 _buffer 값에 접근을 할 수 있을까?

그러지 않고서야 SendBuffer의 Close를 호출 했을 때 SendBuffer에 있던 _buffer에 Open 이후 밀어 넣었던 값들이 있을 수는 없기 때문이다.

image

SendBufferHelper를 통해 Open만 해줬을 뿐인데 어떻게 s가 ThreadLocalStorage에 있는 SendBuffer의 _buffer 값에 접근을 할 수 있는지 궁금합니다.

s는 분명 구조체 타입이고,

TryWriteBytes로 s에 패킷 데이터를 밀어 넣어준 다음에

SendBufferHelper.Close를 했는데

어떻게 s의 정보가 ThreadLocalStorage에 있는 SendBuffer의 _buffer에도 남아 있는지 궁금합니다.

디버깅을 찍어보니깐 SendBuffer.Close()가 호출이 될 때,

새롭게 생성한 ArraySegment의 인자로 들어가는 _buffer 값이 s의 값이더라구요.

패킷의 데이터 실제로 넣은 곳은 s인데 어떻게 ThreadLocalStorage에 있는 SendBuffer의 _buffer 값에도 영향을 주는 것인지..

ArraySegment가 class 타입이면 SendBufferHelper.Open으로 생성한 ArraySegment 를 참조해서 가능하겠거니

이해가 되겠는데 struct 타입이라 도저히 이해가 되지 않아요..ㅠ

어떤 마법과도 같은 일이 있었는지 알려주시면 감사하겠습니다 :D

답변

_buffer가 배열 타입이기 때문에 실제로 SendBuffer.Open()이 되었을 때 전달하는 값은 _buffer의 주소값이다.

따라서 s는 구조체지만 내부에 있는 s.Array에는 _buffer의 주소를 가리키고 있기 때문에 s.Array의 값을 통해 실제 _buffer의 값을 바꿔주는 것