[C# 서버] 소켓 프로그래밍 - PacketSession

이정석·2023년 8월 18일
0

CSharpServer

목록 보기
9/13

PacketSession

[소켓프로그래밍 - Session]에서 서버와 클라이언트 사이의 통신 Session을 구현하였는데 서버-클라이언트간의 패킷 통신같은 경우 받은 데이터를 패킷으로 조립하는 과정이 필요하다.

물론 사용할 때마다 패킷을 조립하는 부분을 구현해도 되지만 코드의 중복이나 구현시간을 고려하면 미리 PacketSession이라는 Session의 한 종류를 구현해 놓는게 더 효율적이다.

1. TCP

Session을 구현한 TCP는 스트림 지향적인 프로토콜로 TCP는 데이터를 ByteStream으로 처리한다. ByteStream으로 처리한다는 것은 메세지의 경계를 구분하지 않는다는 것을 의미한다. 이로인해 아래 그림과 같은 상황이 발생할 수 있다.

데이터를 보내는 From초록색x3-파란색x2-빨간색x2를 보냈지만 데이터를 받는 To초록색x2-초록색x1, 파란색x2-빨간색x2 또는 초록색x1-초록색x2, 파란색x1-파란색x1, 빨간색x2와 같이 색깔의 순서는 같지만 같은 색깔이 한번에 Receive된다는 보장이 없다.

하지만, 색깔의 순서가 같다는 점을 이용해 앞으로 들어오는 메세지의 크기를 패킷의 처음 데이터로 입력한다면 FromTo에게 보내는 데이터를 메세지 단위로 구분할 수 있다.

2. Packet 구조

먼저 패킷에 필수적으로 포함되는 정보는 다음과 같다.

  1. 메세지 구분을 위한 패킷 사이즈
  2. 패킷의 종류를 구분하기 위한 Packet ID
  3. 패킷 데이터
[Packet Size(2 Byte)] [Packet ID(2 Byte)] [Data0...] ...

3. 코드

    public abstract class PacketSession : Session
    {
        public static readonly int HeaderSize = 2;

        public sealed override int OnRecv(ArraySegment<byte> buffer)
        {
            int processLen = 0;

            while(true)
            {
                // 최소한 헤더는 파싱할 수 있는가?
                if (buffer.Count < HeaderSize)
                    break;

                // 패킷이 완전체로 도작했는가?
                ushort dataSize = BitConverter.ToUInt16(buffer.Array, buffer.Offset);
                if (buffer.Count < dataSize)
                    break;

                // 패킷이 조립 가능하다.
                OnRecvPacket(new ArraySegment<byte>(buffer.Array, buffer.Offset, dataSize));

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

            return processLen;
        }

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

PacketSession은 Session을 상속받는 구조로 Receive시에 실행될 OnRecv를 구현하고 이후 PacketSession을 상속받는 컨텐츠 Session에서 Packet을 Receive받을 때의 동작을 구현하도록 추상 메소드인 OnRecvPacket을 두었다.

버퍼에 들어온 데이터가 패킷 하나 이상 들어올 수 있고 2개 이상의 패킷이 한번에 들어올 수 있기 때문에 여러 상황들에 대한 처리가 이뤄저야 한다.

  1. 매우 적은 데이터가 들어올 경우
    // 최소한 헤더는 파싱할 수 있는가?
    if (buffer.Count < HeaderSize)
        break;

패킷의 처음 2 Byte는 전체 패킷의 사이즈가 들어와야 하기 때문에 2 Byte보다 더 적은 데이터가 들어왔을 때를 처리한다.

  1. 패킷의 사이즈만큼 데이터가 들어왔는가?
    // 패킷이 완전체로 도작했는가?
    ushort dataSize = BitConverter.ToUInt16(buffer.Array, buffer.Offset);
    if (buffer.Count < dataSize)
        break;

하나의 패킷이 덜 들어왔을 때를 처리해준다.

  1. 패킷이 수신됐다.
    // 패킷이 조립 가능하다.
    OnRecvPacket(new ArraySegment<byte>(buffer.Array, buffer.Offset, dataSize));

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

dataSize는 패킷시작부터 마지막 비트까지의 전체길이를 의미하기 때문에 부분 배열의 범위는 [Offset - Offset+dataSize]이다. OnRecvPacket을 발생시키면 하나의 패킷처리가 끝났으므로 Buffer의 위치를 수정한다.


참고문헌

  1. 배현직. 게임 서버 프로그래밍 교과서. 길벗, 2020
profile
게임 개발자가 되고 싶은 한 소?년

0개의 댓글