C#

PacketSession

monstro 2024. 12. 31. 11:44
728x90
반응형

TCP 통신환경에서는

상황을 고려하여 패킷을 완전하게 보내지 않고 내용의 일부를 잘라 전송합니다.

이때, 패킷이 완전히 도착하지 않고 잘려서 오는 경우에

잘려진 패킷을 올바르게 결합하는 여러 방법이 존재합니다.

 

이번 프로젝트에서는 패킷 별로 ID를 설정하여 잘려진 패킷을 결합하는 방식을 구현해보도록 하겠습니다.

ID를 설정하는 경우에 인자로 패킷의 사이즈와 패킷의 ID를 우선적으로 설정해야 합니다.

그리고 이렇게 우선적으로 설정되는 패킷의 메모리Header라고 부릅니다.

 

Header를 구현하는 경우에는 용량을 최대한 줄이고,

Header의 메모리Header에 기록할지 결정해야 합니다.

이번 프로젝트에서는 Header에 Header의 메모리도 기록하겠습니다.

따라서 Header의 메모리 크기도 Header에 포함하도록 하겠습니다.

 

1) Session

기존의 Session 코드를 수정하여 Session에 패킷을 포함하는 기능을 추가하도록 하겠습니다.

public abstract class PacketSession : Session
{
    public static readonly int HeaderSize = 2;
    public sealed override int OnRecv(ArraySegment<byte> buffer)
    {
        // 처리한 바이트의 길이
        int processLength = 0;
        
        // 패킷을 처리
        while (true)
        {
            // 최소 패킷의 헤더(=패킷의 용량 + 패킷의 아이디)를 갖고 있는지 확인
            if (buffer.Count < HeaderSize)
                break;

            // 패킷이 분할되어 도착하였는지 확인
            ushort dataSize = BitConverter.ToUInt16(buffer.Array, buffer.Offset);
            if (buffer.Count < dataSize)
                break;

            // 로직이 여기까지 수행되었다면 패킷을 조립할 수 있음 
            // 패킷의 조립방법에는 새로운 인스턴스를 생성하거나, Slice 메서드를 통해 기존의 것을 자르거나 하는 방법이 존재함
            // ArraySegment는 클래스가 아닌 구조체이므로 스택에 생성되기 때문에 부담없이 생성해도 됨
            OnRecvPacket(new ArraySegment<byte>(buffer.Array, buffer.Offset, dataSize));

            processLength += dataSize;
            buffer = new ArraySegment<byte>(buffer.Array, buffer.Offset + dataSize, buffer.Count - dataSize);
        }

        return processLength;
    }

    public abstract void OnRecvPacket(ArraySegment<byte> buffer);
}

 

위와 같이 Session을 상속받아 Session의 기능을 갖되, 

패킷의 기능도 수행할 수 있도록 로직을 구성하였습니다.

 

이때 OnRecv에 사용된 Sealed 수식자OnRecv가 PacketSession에서만 오버라이드되도록 설정합니다.

PacketSession외에 Session에서 상속된 다른 형제자매들은 OnRecv를 오버라이드할 수 없습니다.

 

2) Server

// 패킷의 인자를 설정하는 경우, 최대한 용량을 줄여주는 것이 좋음
class Packet
{
    // 패킷의 용량
    public ushort _packetSize;
    // 패킷의 아이디
    public ushort _packetID;
}

class GameSession : PacketSession
{
    // 연결되는 경우 호출
    public override void OnConnected(EndPoint endPoint)
    {
        Console.WriteLine($"[Connect from] : {endPoint}");

        Packet packet = new Packet() { _packetSize = 4, _packetID = 1};

        ArraySegment<byte> openSegment = SendBufferHelper.Open(4096);
        byte[] buffer1 = BitConverter.GetBytes(packet._packetSize);
        byte[] buffer2 = BitConverter.GetBytes(packet._packetID);
        Array.Copy(buffer1, 0, openSegment.Array, openSegment.Offset, buffer1.Length);
        Array.Copy(buffer2, 0, openSegment.Array, openSegment.Offset + buffer1.Length, buffer2.Length);
        ArraySegment<byte> sendBuffer = SendBufferHelper.Close(packet._packetSize);

        //Send(sendBuffer);
        Thread.Sleep(5000);
        Disconnect();
    }

    // 연결이 끊기는 경우 호출
    public override void OnDisconnected(EndPoint endPoint)
    {
        Console.WriteLine($"[Disconnect from] : {endPoint}");
    }

    // 클라로 데이터를 보내는 경우 호출
    public override void OnSend(int numOfBytes)
    {
        Console.WriteLine($"Transferred Bytes : {numOfBytes}");
    }

    public override void OnRecvPacket(ArraySegment<byte> buffer)
    {
        ushort size = BitConverter.ToUInt16(buffer.Array, buffer.Offset);
        ushort id = BitConverter.ToUInt16(buffer.Array, buffer.Offset + 2);
        Console.WriteLine($"RecvPacket has Id : {id} and Size : {size}");
    }
}

 

Server 코드의 클라이언트 세션을 설정하는 로직을 일부 수정하였습니다.

ushort는 short 자료형이지만, 음수 표현을 제외하므로 short 자료형보다 더 많은 크기를 사용가능합니다.

2바이트의 크기를 지녔으므로 현재 패킷은 총 4바이트의 크기를 지니게 됩니다.

 

추가적으로 클라이언트 세션의 Send를 막아 서버에서 클라이언트로 전송하지는 않게 하겠습니다.

 

3) Client

// 패킷의 인자를 설정하는 경우, 최대한 용량을 줄여주는 것이 좋음
class Packet
{
    // 패킷의 용량
    public ushort _packetSize;
    // 패킷의 아이디
    public ushort _packetID;
}

class GameSession : Session
{
    // 연결되는 경우 호출
    public override void OnConnected(EndPoint endPoint)
    {
        Console.WriteLine($"[Connect from] : {endPoint}");

        Packet packet = new Packet() { _packetSize = 4, _packetID = 1 };

        // 서버로 전송
        for (int i = 0; i < 5; i++)
        {
            ArraySegment<byte> openSegment = SendBufferHelper.Open(4096);
            byte[] buffer1 = BitConverter.GetBytes(packet._packetSize);
            byte[] buffer2 = BitConverter.GetBytes(packet._packetID);
            Array.Copy(buffer1, 0, openSegment.Array, openSegment.Offset, buffer1.Length);
            Array.Copy(buffer2, 0, openSegment.Array, openSegment.Offset + buffer1.Length, buffer2.Length);
            ArraySegment<byte> sendBuffer = SendBufferHelper.Close(packet._packetSize);

            Send(sendBuffer);
        }
    }

    // 연결이 끊기는 경우 호출
    public override void OnDisconnected(EndPoint endPoint)
    {
        Console.WriteLine($"[Disconnect from] : {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}");
    }
}

 

Client의 서버 세션 역시 패킷을 설정하여 사용하도록 하겠습니다.

총 5개의 패킷을 생성하여 서버로 전송하도록 하겠습니다.

 

최종 실행결과는 다음과 같습니다.

728x90
반응형

'C#' 카테고리의 다른 글

직렬화(Serialization) 2  (0) 2025.01.01
직렬화(Serialization) 1  (1) 2025.01.01
RecvBuffer와 SendBuffer  (0) 2024.12.30
TCP - UDP의 이해  (0) 2024.12.25
Connect를 위한 Connector 생성  (0) 2024.12.25