수업

using System;
using System.Collections.Generic;
using System.Linq;
using System.Text;
using System.Threading.Tasks;

namespace ServerCore
{
    public class SendBufferHelper
    {
        // 쓰레드 로컬로 만듬 전역은 전역인데 자신의 영역에 할당
        // TLS(ThreadLocalStorage) 사용: 스레드마다 고유한 버퍼를 할당
        public static ThreadLocal<SendBuffer> CurrentBuffer = new ThreadLocal<SendBuffer>(() => { return null; });

        public static int ChunkSize { get; set; } = 4096; // 기본 버퍼 크기

        public static ArraySegment<byte> Open(int reserveSize)
        {
            // 버퍼가 없거나 사용 가능한 공간이 부족하면 새로 할당
            if (CurrentBuffer.Value == null)
                CurrentBuffer.Value = new SendBuffer(ChunkSize);

            if (CurrentBuffer.Value.FreeSize < reserveSize)
                CurrentBuffer.Value = new SendBuffer(ChunkSize);

            return CurrentBuffer.Value.Open(reserveSize);
        }

        public static ArraySegment<byte> Close(int usedSize)
        {
            return CurrentBuffer.Value.Close(usedSize);
        }
    }
    public class SendBuffer
    {
        byte[] _buffer;
        int _usedSize = 0;  // 현재까지 사용된 크기


        public int FreeSize { get { return _buffer.Length - _usedSize; } }
        //public int FreeSize => _buffer.Length - _usedSize; // 남은 공간 크기 반환
        
        public SendBuffer(int chunkSize)
        {
            _buffer = new byte[chunkSize]; // 초기화 시 버퍼 크기 설정
        }

        // Open: 사용할 공간을 예약 (reserveSize만큼 공간이 남아 있는지 확인 후 제공)
        public ArraySegment<byte> Open(int reserveSize)
        {
            if (reserveSize > FreeSize)
                return null;  // 공간 부족 시 null 반환

            // 작업할 영역을 찝어줌
            return new ArraySegment<byte>(_buffer, _usedSize, reserveSize);
        }

        // Close: 사용한 공간을 확정하고, 사용한 크기만큼 _usedSize 증가
        // close 공간을 확정함
        public ArraySegment<byte> Close(int usedSize)
        {
            // 사용한 공간을 찝어줌
            ArraySegment<byte> segment = new ArraySegment<byte>(_buffer, _usedSize, usedSize);
            // 사용한 공간만큼 늘려줌
            _usedSize += usedSize;
            // 범위를 반환
            return segment;
        }
    }

    
}

using ServerCore;
using System.Net;
using System.Net.Sockets;
using System.Text;

namespace DummyClient
{
    #region SendBuffer
    class Knight
    {
        public int hp;
        public int attack;
        // 고정 아닌 예
        public string name;
        public List<int> skills = new List<int>();
        // 가변적이여서 예측이 어려움
        
    }
    #endregion

    #region PacketSession
    class Packet
    {
        // int 보다 4바이트 아낌
        // 만약 10000명이면 40000바이트를 아낌
        public ushort size;
        public ushort packetId;
    }
    #endregion
    class GameSession : Session
    {
        public override void OnConnected(EndPoint endPoint)
        {
            Console.WriteLine($"OnConnected: {endPoint}");

            Knight knight = new Knight() { hp = 100, attack = 10};

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

            // 버퍼사이즈를 어떻게 할까
            // 버퍼사이즈 고정사이즈가 아닌 경우가 생김
            // 무작정 크게 잡아 놓는것은 낭비임
            // 개선하기 위해서 큰 덩어리를 만들고 차츰 차츰 사용해갈 수 있게 만듬
            // [][][][][][][][][][][][][] 큰 덩어리를 잘라서 사용
            //byte[] sendBuff = new byte[1024];
            byte[] buffer = BitConverter.GetBytes(knight.hp);
            byte[] buffer2 = BitConverter.GetBytes(knight.attack);
            //[buffer][][][][buffer2 ]
            //Array.Copy(buffer, 0, sendBuff, 0, buffer.Length);
            //Array.Copy(buffer2, 0, sendBuff, 4, buffer2.Length);

            Array.Copy(buffer, 0, openSegment.Array, openSegment.Offset, buffer.Length);
            Array.Copy(buffer2, 0, openSegment.Array, openSegment.Offset + buffer.Length, buffer2.Length);
            //닫아줘
            // 8바이트 컨펌
            ArraySegment<byte> sendBuff = SendBufferHelper.Close(buffer.Length + buffer2.Length);

            // if 100명
            // 1 ->  100명
            // 100 ->  이동 패킷 100 * 100 = 1만

            Packet packet = new Packet() { size = 4, packetId = 7 };
            // 서버에 5번 메시지를 전송

            Send(sendBuff);
            Thread.Sleep(1000);
            Disconnect();

            #region PacketSession
            for (int i = 0; i < 5; i++)
            {
                //byte[] sendBuff = Encoding.UTF8.GetBytes($"Hello World! {i}");
                //Send(sendBuff);

                
                //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> sendBuff = SendBufferHelper.Close(packet.size);
                //Send(sendBuff);
               
            }
            #endregion
        }

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

            Knight knight = new Knight() { hp = 100, attack = 10 };

            // SendBufferHelper를 사용하여 버퍼 할당
            ArraySegment<byte> openSegment = SendBufferHelper.Open(4096);

            byte[] buffer = BitConverter.GetBytes(knight.hp);
            byte[] buffer2 = BitConverter.GetBytes(knight.attack);

            // 데이터 복사 (각 필드의 위치를 고려하여 복사)
            Array.Copy(buffer, 0, openSegment.Array, openSegment.Offset, buffer.Length);
            Array.Copy(buffer2, 0, openSegment.Array, openSegment.Offset + buffer.Length, buffer2.Length);

            // 사용한 크기만큼 Close() 호출하여 실제 전송할 버퍼 생성
            ArraySegment<byte> sendBuff = SendBufferHelper.Close(buffer.Length + buffer2.Length);

            Send(sendBuff);
            Thread.Sleep(1000);
            Disconnect();

        }

        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}");
        }
    }
    class Program
    {
        static void Main(string[] args)
        {
            // DNS
            string host = Dns.GetHostName();
            IPHostEntry ipHost = Dns.GetHostEntry(host);
            IPAddress ipAddr = ipHost.AddressList[0];
            IPEndPoint endPoint = new IPEndPoint(ipAddr, 7777);

            Connector connector = new Connector();
            connector.Connect(endPoint, () => { return new GameSession(); });

            while (true)
            {
                // 휴대폰 설정
                Socket socket = new Socket(endPoint.AddressFamily, SocketType.Stream, ProtocolType.Tcp);


                try
                {
                    //// 문지기한테 입장 문의
                    //socket.Connect(endPoint);

                    //Console.WriteLine($"Connected to {socket.RemoteEndPoint.ToString()}");

                    //// 보낸다
                    //for (int i = 0; i < 5; i++)
                    //{
                    //    byte[] sendBuff = Encoding.UTF8.GetBytes($"Hello Server! {i}");
                    //    int sendByte = socket.Send(sendBuff);
                    //}
                    //// 받는다
                    //byte[] recvBuff = new byte[1024];
                    //int recvBytes = socket.Receive(recvBuff);
                    //string recvData = Encoding.UTF8.GetString(recvBuff, 0, recvBytes);
                    //Console.WriteLine($"[From Server] {recvData}");

                    //socket.Shutdown(SocketShutdown.Both);
                    //socket.Close();
                }
                catch (Exception e)
                {
                    Console.WriteLine(e.ToString());
                }

                Thread.Sleep(100);
            }
        }
    }
}

좋습니다. 지금부터 사용자가 요청하신 조건과 형식에 따라,
총 3개의 분석글을 완전히 통합한 PacketSession 강의 블로그 통합 강의 교재를 제공하겠습니다.

이 자료는 강의 내용을 완벽하게 재해석하고, 실제 강의에서 사용하는 스타일로 구성되며,
내용이 방대하므로 총 2부로 나누어 작성하겠습니다.
이번에는 1부를 작성하며, 이어서 2부도 자연스럽게 연결될 수 있도록 구성하겠습니다.


🎓 PacketSession 강의 통합 교재 (1부 / 총 2부)


✅ 주제

  • 이 강의의 주제는 TCP 스트림 기반 네트워크 통신에서의 안정적인 패킷 조립 구조 구현입니다.
  • 특히 TCP는 데이터가 연속적인 스트림으로 전달되기 때문에,
    개별 패킷을 구분하고 처리하기 위한 헤더 기반 수신 구조,
    그리고 이를 Session 구조에 안전하게 통합하는 PacketSession 클래스의 설계 방식이 핵심입니다.

📚 개념

TCP는 스트림이다

  • TCP는 데이터 전송의 신뢰성과 순서를 보장하지만,
    우리가 보낸 1개의 패킷이 받는 쪽에서는 여러 번 나눠서 도착하거나,
    한 번에 여러 개가 함께 붙어서 올 수도 있는 구조입니다.

예를 들어, 우리가 보낸 패킷이 다음과 같이 조각나올 수 있습니다:

Client가 보낸 실제 패킷: [size][packetId][payload]
Server 수신 분할 예:
  1) [size]만 먼저 옴
  2) [size][packetId]까지 옴
  3) 여러 개가 붙어서 [size][packetId][payload][size][packetId][payload] 형태로 옴

➡️ 문제는 도착한 데이터의 경계를 알 수 없다는 것입니다.


패킷 구분을 위한 구조 설계

  • 우리는 모든 패킷 앞에 ushort size를 먼저 두어 패킷의 전체 길이를 나타냅니다.
  • 이어서 ushort packetId를 두어 패킷의 종류를 구분합니다.

즉, 하나의 패킷은 항상 다음과 같은 고정 포맷을 갖습니다:

// 패킷 포맷 (총 최소 4바이트)
ushort size;       // 전체 패킷 길이 (자기 자신 포함)
ushort packetId;   // 이 패킷이 무엇을 의미하는지
...payload...      // 이후 데이터

이 구조는 모든 패킷 조립과 해석의 기준이 됩니다.


PacketSession의 탄생

  • 위와 같은 수신 문제를 해결하기 위해,
    Session 클래스에서 수신 구조를 확장한 PacketSession을 설계합니다.
  • 핵심 아이디어는 다음과 같습니다:
1) 수신 버퍼에 최소한 [size] 만큼의 데이터가 들어왔는지 확인
2) size만큼 데이터가 모두 도착했으면 → 패킷 완성 → OnRecvPacket() 호출
3) 남은 데이터가 있다면 루프를 돌아 계속 처리

➡️ 즉, PacketSession은 헤더 기반으로 패킷을 안전하게 분리하고,
완성된 데이터만 컨텐츠 계층으로 넘겨주는 핵심 클래스입니다.


📘 용어 정리

용어설명
TCP 스트림연속된 바이트 흐름. 패킷 단위가 존재하지 않음
Packetsize + packetId + payload 구조의 전송 단위
PacketSessionSession을 상속받아, 수신된 데이터를 패킷 단위로 분리하는 추상 클래스
HeaderSize패킷의 최소 헤더 크기. 현재는 ushort size = 2바이트
OnRecv()Session 클래스의 수신 콜백 함수. PacketSession에서는 sealed 처리
OnRecvPacket()완성된 하나의 패킷이 도착했을 때 호출되는 추상 함수
ArraySegment<byte>byte 배열의 특정 범위를 참조하기 위한 C# 구조체
BitConverterbyte 배열 ↔ ushort, int 등의 변환을 담당하는 유틸리티

💻 코드 분석

📌 PacketSession 클래스 선언

public abstract class PacketSession : Session
{
    public static readonly int HeaderSize = 2; // 현재는 size(ushort)만 기준
  • Session 클래스를 기반으로 확장한 추상 클래스입니다.
  • 현재는 HeaderSize = 2ushort size만을 기준으로 패킷 구분을 수행합니다.

📌 수신 처리 메서드 - OnRecv()

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;

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

🔍 설명 흐름

  1. sealed override: 이 함수는 PacketSession 내부 로직으로 고정 → 하위 클래스에서 오버라이드 불가
  2. while (true) 루프: 한 번에 여러 개 패킷이 들어오는 경우까지 처리 가능
  3. buffer.Count < HeaderSize: 최소 2바이트 이상 들어오지 않으면 패킷 크기조차 읽을 수 없음 → 대기
  4. BitConverter.ToUInt16(): 앞 2바이트에서 전체 패킷 길이를 추출
  5. buffer.Count < dataSize: 전체 패킷이 아직 다 안 들어왔으면 → 대기
  6. OnRecvPacket(...): 완성된 패킷만 넘겨줌
  7. processLength 누적 + buffer를 남은 영역으로 조정 → 다음 패킷 반복 처리 가능
  8. 최종적으로 사용한 총 바이트 수 반환

📌 OnRecvPacket 추상 메서드

public abstract void OnRecvPacket(ArraySegment<byte> buffer);
  • 완성된 패킷이 도착하면 호출됩니다.
  • 컨텐츠 계층에서 이 메서드를 구현하여,
    패킷 구조를 파싱하고 packetId에 따라 분기 처리하면 됩니다.

✅ 핵심 요약 (1부)

항목설명
TCP의 문제패킷 단위가 없기 때문에 조각 수신이 발생함
해결 방법size 기반 헤더 구조로 패킷을 조립
PacketSession 역할수신된 데이터를 완성된 패킷 단위로 분리해서 넘겨줌
OnRecv()봉인(sealed)되어 내부에서만 패킷 조립
OnRecvPacket()완성된 패킷만 콘텐츠 계층으로 전달하는 확장 포인트
구조적 이점연속 수신, 조각 수신, 누락 등 모든 상황에 안전하게 대응 가능

🧠 마무리 정리 (1부)

  • 우리가 보내는 단위(패킷)와 TCP가 전달하는 단위(스트림)는 다릅니다.
  • 그래서 반드시 size 기반의 구조를 도입하고,
    이를 자동으로 해석해주는 구조가 필요하며,
  • PacketSession은 바로 “Session 단에서 패킷 완성 여부를 책임지고 처리하는 계층”입니다.
  • 이를 통해 콘텐츠 계층에서는 단지 OnRecvPacket()에서 완성된 데이터를 받아 쓰기만 하면 되는 구조를 만들 수 있습니다.

좋습니다. 앞서 1부에서는 PacketSession 구조의 설계 배경과 TCP 수신 버퍼에서 어떻게 패킷을 조립하고 분리하는지를 상세히 학습했습니다. 이제 이어지는 2부에서는 실제 서버와 클라이언트가 PacketSession을 활용하여 패킷을 전송하고 수신 처리하는 흐름, 그리고 그 과정에서 사용되는 SendBufferHelper, GameSession, Packet 구조까지 포함하여 모든 전송 프로토콜 흐름을 실습 예제로 이해해보겠습니다.


🎓 PacketSession 강의 교재 (2부 / 총 2부)


✅ 주제

  • 본 강의의 핵심은 PacketSession 기반 송수신 흐름의 실제 적용 예시입니다.
  • 특히 서버와 클라이언트에서 패킷을 조립하고, 보내고, 수신한 후 OnRecvPacket()을 통해 해석하는 일련의 흐름을 실습을 통해 분석합니다.
  • 또한 SendBufferHelper를 활용한 전송 데이터 조립 방식과, PacketLoginOkPacket 클래스의 확장 구조까지 다룹니다.

📚 개념

sizepacketId를 먼저 보내는가?

TCP는 메시지 단위가 아닌 바이트 스트림이기 때문에, “어디서부터 어디까지가 하나의 패킷인가”를 알려주는 기준이 반드시 필요합니다.
이를 위해 우리는 모든 패킷 앞에 [size][packetId]를 붙여 전송합니다.

  • size: 패킷 전체 길이(헤더 + 바디 포함)
  • packetId: 이 패킷이 어떤 의미인지 식별할 ID

이 구조 덕분에 우리는 여러 개 패킷이 연속으로 오거나, 조각나서 올 경우에도 안정적으로 패킷을 분리하고 처리할 수 있습니다.


📘 용어 정리

용어설명
Packet모든 패킷의 공통 헤더 구조 (size + packetId)
LoginOkPacketPacket을 상속받아 실제 게임 컨텐츠용 패킷을 구현한 예시
SendBufferHelperThreadLocal 기반의 TLS 버퍼를 관리하며, 송신 데이터를 효율적으로 구성
BitConverter.GetBytes()숫자형 데이터를 byte 배열로 변환
Array.Copy()한 byte 배열에서 다른 배열로 원하는 만큼 복사
OnRecvPacket()완성된 하나의 패킷이 도착했을 때 호출되는 함수

💻 코드 분석

🔷 Packet 및 LoginOkPacket 구조

public class Packet
{
    public ushort size;
    public ushort packetId;
}
  • 네트워크를 통해 오가는 모든 데이터는 이 구조를 기반으로 시작합니다.
  • ushort 사용은 공간을 절약하고 네트워크 효율을 높이기 위한 선택입니다.
public class LoginOkPacket : Packet
{
    // 향후 캐릭터 목록, 스킬 목록 등 게임 데이터를 여기에 추가
}
  • 컨텐츠 단에서는 이렇게 Packet을 상속받아 다양한 패킷을 정의하게 됩니다.

🔷 서버측 GameSession 연결 시 패킷 전송 흐름

public override void OnConnected(EndPoint 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);
}

💬 설명

  • Packet 생성 후 SendBufferHelper.Open()으로 버퍼를 확보합니다.
  • BitConverter.GetBytes()를 통해 ushort 데이터를 byte[]로 변환하고,
  • Array.Copy()로 버퍼에 삽입한 뒤,
  • SendBufferHelper.Close()로 실제 사용한 범위를 확정지어 Send()를 호출합니다.

🔷 OnRecvPacket - 수신한 패킷 해석

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($"Receive Packet size : {size}, ID : {id}");
}
  • PacketSession.OnRecv()이 내부에서 완성된 패킷만 이 메서드로 넘겨줍니다.
  • 우리는 받은 buffer에서 sizepacketId를 추출하여 처리할 수 있습니다.

🔷 DummyClient - 클라이언트에서 연속 패킷 전송

for (int i = 0; i < 5; i++)
{
    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);
}
  • 동일한 패킷을 5회 연속 전송하여 연속 수신 상황을 테스트합니다.
  • 이때 서버에서는 PacketSession이 작동하여 각각을 정확하게 분리해 전달합니다.

🔷 DummyClient.OnRecv - 문자열 수신 디버깅

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;
}
  • 현재는 문자열로 단순 출력하는 구조입니다.
  • 추후 DummyClient도 PacketSession을 상속받아 구조화된 패킷을 처리해야 합니다.

✅ 핵심 요약

항목설명
Packet 구조[ushort size][ushort packetId]의 고정 헤더를 모든 패킷에 삽입
송신 흐름Open → BitConverter + Copy → Close → Send
수신 흐름PacketSession.OnRecv()이 패킷 조립 → OnRecvPacket() 호출
연속 처리여러 패킷이 붙어와도 하나씩 정확하게 분리해 처리 가능
확장 기반Packet을 상속한 다양한 패킷 정의로 게임 컨텐츠 전송 가능

🧠 마무리 정리 (2부)

  • PacketSession은 TCP의 스트림 기반 수신 문제를 해결하는 궁극적인 수단입니다.
  • [size][packetId][payload] 형식을 사용하면, 수신이 조각나든, 연달아 붙든 상관없이 안정적으로 분리하고 처리할 수 있습니다.
  • 서버와 클라이언트 모두 같은 방식으로 패킷을 조립하고, 파싱하므로 전송 프로토콜의 일관성이 유지됩니다.
  • 앞으로 우리는 이 구조 위에 다양한 게임 패킷 (예: LoginOk, Move, Attack 등)을 쌓아가며 MMORPG 통신의 기반을 확장하게 될 것입니다.

🎓 정리하면, PacketSession은 게임 서버 개발에 있어 패킷 단위 통신을 가능하게 해주는 구조적 핵심 클래스입니다.
이 구조 없이 TCP로 안정적인 데이터 처리를 기대할 수 없고,
이 구조를 제대로 익혀야만 수만 명의 유저와도 충돌 없이 통신할 수 있는 서버를 설계할 수 있습니다.


profile
李家네_공부방

0개의 댓글