수업

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

    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만

            
            // 서버에 5번 메시지를 전송

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

            
        }

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

        }
    }
}

좋습니다. 지금부터는 요청하신 조건과 형식에 맞춰, "SendBuffer" 강의 블로그 분석글들을 완전히 통합한 강의 교재 형식의 문서를 작성하겠습니다.
한 글자도 빠짐없이, 모든 코드와 설명을 정리하고, 실제 강의에서 사용할 수 있는 스타일로 구성하였습니다.

내용이 매우 방대한 관계로, 문서는 총 2부 구성입니다.
지금은 그 중 1부를 작성하며,

📌 SendBuffer의 설계 배경, 구조, ThreadLocal 기반의 동작 원리, Open/Close 흐름, SendBufferHelper 클래스 역할까지 다룹니다.


🎓 SendBuffer 통합 학습 교재 (1부 / 총 2부)


✅ 주제

  • 이 강의의 핵심 주제는 멀티스레드 환경에서 효율적으로 송신 데이터를 관리하기 위한 SendBuffer 구조의 설계와 사용법입니다.
  • 특히, Chunk 단위의 대형 버퍼를 스레드 단위로 나누어 예약(Open)-확정(Close)하는 구조를 학습하고,
  • 이를 통해 메모리 낭비 없이, 복사 최소화 방식으로 고속 전송이 가능한 송신 구조를 구축하는 것이 목표입니다.

📚 개념

SendBuffer가 필요한 이유

서버에서 데이터를 보낼 때, 매번 new byte[]로 배열을 생성하면 다음과 같은 문제가 생깁니다:

  • GC 부담이 커지고,
  • 대규모 서버(MMO)에서 수천, 수만 개 패킷을 전송하는 상황에서는
  • 할당/복사/삭제의 반복이 치명적인 병목이 됩니다.

이를 해결하기 위해 SendBuffer는 아래와 같은 전략을 사용합니다:

  1. Chunk 단위의 큰 버퍼를 미리 확보하고
  2. 필요할 때마다 잘라서 사용하는 예약-확정 구조(Open/Close)
  3. 각 스레드마다 고유한 버퍼를 할당하여 멀티스레드 환경에서 충돌 방지

📘 용어정리

용어설명
SendBuffer고정 크기 버퍼에서 필요한 만큼 잘라 쓰는 송신 버퍼
Chunk미리 할당한 큰 버퍼 덩어리
Open()사용할 공간을 미리 예약하는 함수
Close()실제 사용된 크기를 확정하는 함수
ArraySegment배열을 복사하지 않고, 범위만 지정하는 포인터처럼 사용하는 클래스
ThreadLocal각 스레드마다 독립적으로 값을 가지는 저장소

💻 코드 분석


🔷 1. SendBuffer 클래스

public class SendBuffer
{
    byte[] _buffer;
    int _usedSize = 0;

    public SendBuffer(int chunkSize)
    {
        _buffer = new byte[chunkSize];
    }

    public int FreeSize => _buffer.Length - _usedSize;

    public ArraySegment<byte> Open(int reserveSize)
    {
        if (reserveSize > FreeSize)
            return null;
        return new ArraySegment<byte>(_buffer, _usedSize, reserveSize);
    }

    public ArraySegment<byte> Close(int usedSize)
    {
        ArraySegment<byte> segment = new ArraySegment<byte>(_buffer, _usedSize, usedSize);
        _usedSize += usedSize;
        return segment;
    }
}

🔍 해설

  • _buffer: 실제 데이터를 담는 바이트 배열
  • _usedSize: 지금까지 사용한 크기 (커서 역할)
  • FreeSize: 남은 공간. Open() 전에 여유 공간 판단에 사용
✅ Open()
  • 지정한 크기만큼 공간을 예약합니다.
  • 실제로 사용하지 않더라도 우선 확보만 하는 단계입니다.
✅ Close()
  • 예약된 공간 중에서 실제 사용한 만큼만 확정합니다.
  • _usedSize를 증가시켜 다음 예약 공간의 시작점을 이동시킵니다.

SendBuffer1회용입니다.
이미 Session.SendQueue나 소켓에 참조된 상태일 수 있으므로 되감기나 재사용은 금지입니다.


🔷 2. SendBufferHelper 클래스

public static class SendBufferHelper
{
    public static ThreadLocal<SendBuffer> CurrentBuffer = new ThreadLocal<SendBuffer>(() => { return null; });
    public static int ChunkSize { get; set; } = 4096 * 100;

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

🔍 해설

  • ThreadLocal<SendBuffer>: 스레드마다 고유한 버퍼를 보유하게 해주는 TLS 구조
  • ChunkSize: 할당할 전체 버퍼 크기 (기본: 4096 * 100 = 40KB)
  • Open():
    • 현재 스레드의 버퍼가 없거나,
    • 남은 공간이 부족한 경우 → 새 SendBuffer 할당
    • 있으면 기존 버퍼에서 예약
  • Close():
    • 사용한 크기만큼 확정

🔄 정리된 동작 흐름

[1] SendBufferHelper.Open(예약할 크기)
   ↓
[2] ArraySegment<byte>를 리턴하여 데이터 복사 대상 확보
   ↓
[3] 사용자 → 복사할 데이터를 해당 ArraySegment에 기록
   ↓
[4] SendBufferHelper.Close(실제 사용 크기)
   ↓
[5] 확정된 ArraySegment를 Session.Send()에 전달

🔬 실전 예제 흐름

Knight knight = new Knight() { hp = 100, attack = 10 };
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);

ArraySegment<byte> sendBuff = SendBufferHelper.Close(buffer.Length + buffer2.Length);
Send(sendBuff);

설명:

  1. 버퍼 공간 확보(Open)
  2. 직렬화된 데이터를 복사
  3. Close로 사용 범위 확정
  4. Send로 전송

✅ 이 흐름에서 가장 중요한 건 복사가 발생하지 않는다는 점입니다.
메모리를 새로 만들지 않고, 큰 버퍼의 일부분을 잘라서 그대로 전달합니다.


✅ 핵심 요약

항목설명
SendBuffer고정 크기 대형 버퍼, 필요할 때 일부만 잘라 사용
ThreadLocal스레드 고유 SendBuffer 보유, 경합 없음
Open()공간 예약. 복사 안 하고 세그먼트만 할당
Close()사용된 영역 확정. ArraySegment로 리턴
재사용없음. SendBuffer는 1회성 사용 후 버려짐

🧠 마무리 정리 (1부)

  • SendBuffer는 고속 송신을 위한 복사 최소화 + GC 회피 전략입니다.
  • 대형 버퍼를 스레드별로 관리함으로써,
    멀티스레드 환경에서도 안전하게 고속 송신이 가능합니다.
  • Open()Close()는 송신 패킷 조립의 핵심 흐름이며,
    실제 송신 흐름(Session.Send)과 정확하게 연동됩니다.

👉 다음 2부에서는

Session.Send()에서 SendBuffer가 어떻게 사용되는지,
멀티스레드 안전성과 송신 큐 연동 구조,
그리고 C++로 이식할 때의 구조적 개선 방향까지 다루겠습니다.
곧바로 이어서 2부 작성해드리겠습니다.

좋습니다. 지금부터는 SendBuffer 강의 교재 2부 (총 2부 중)를 작성하겠습니다.
1부에서 우리는 SendBuffer의 설계 배경, Open/Close 흐름, TLS(ThreadLocal) 기반 구조, 메모리 재사용 전략까지 배웠습니다.

이번 2부에서는:

SessionGameSession에서 SendBuffer가 어떻게 연동되는지,
멀티스레드 환경에서 안전하게 동작하는 이유,
✅ 그리고 실무에서 고려해야 할 복사 최소화 전략과 설계상의 주의점까지 하나도 빠짐없이 설명합니다.


🎓 SendBuffer 통합 학습 교재 (2부 / 총 2부)


✅ 주제

  • SendBuffer가 실제 네트워크 전송 흐름에서 어떻게 활용되는지 분석합니다.
  • 특히 Session.Send()SendQueueSocket.SendAsync()로 이어지는 과정에서,
    SendBuffer가 어디서 생성되고, 어디서 복사되고, 언제 참조되는지를 흐름 중심으로 파악합니다.
  • 멀티스레드 환경에서도 안전하게 작동하는 이유,
    그리고 C++로 옮겼을 때 개선 가능한 지점까지 함께 살펴봅니다.

📚 개념

왜 SendBuffer는 Session 안이 아니라 밖에서 만들어야 하는가?

RecvBuffer는 세션에 고정되어도 됩니다.
→ 클라이언트가 보낸 데이터는 해당 Session만 처리하면 되기 때문이죠.

하지만 SendBuffer는 그렇지 않습니다.

예시: 유저 A의 위치 변경 정보를 99명에게 뿌려야 할 때,
Session 안에서 매번 복사하면 99번이나 패킷을 복사해야 합니다.

하지만 외부에서 SendBuffer로 한 번만 패킷을 만들고,
각 Session.Send()에 동일한 ArraySegment<byte>를 넘기면,

✅ 복사 없이 99명의 세션에 그대로 전달 가능해집니다.

→ 이것이 SendBuffer가 Session 내부가 아닌 외부에서 생성되어야 하는 이유입니다.


📘 용어정리 (실전 구조 중심)

용어설명
Send()외부에서 완성된 데이터를 전송 큐에 등록하는 함수
_sendQueue전송을 기다리는 버퍼들이 저장되는 큐
_pendingListSocket 전송 직전에 넘겨질 버퍼 리스트
RegisterSend()큐에 담긴 버퍼를 Socket에 전송 등록
OnSendCompleted()비동기 전송이 완료됐을 때 호출되는 콜백
TLS스레드별 고유 버퍼 보관소. ThreadLocal 사용

💻 코드 분석

🔷 1. Session.Send()

public void Send(ArraySegment<byte> sendBuff)
{
    lock (_lock)
    {
        _sendQueue.Enqueue(sendBuff);

        if (_pendingList.Count == 0)
            RegisterSend();
    }
}
  • 외부에서 만들어진 sendBuff_sendQueue에 등록합니다.
  • _pendingList가 비어 있으면 RegisterSend()를 호출해 전송 요청을 등록합니다.

✅ 핵심: sendBuffSendBufferHelper.Close()로 반환된 ArraySegment<byte>, 즉 원본 버퍼 일부를 참조하는 객체입니다.
복사 없이 참조 전달만으로 전송이 가능해집니다.


🔷 2. RegisterSend()

void RegisterSend()
{
    while (_sendQueue.Count > 0)
    {
        ArraySegment<byte> buff = _sendQueue.Dequeue();
        _pendingList.Add(buff);
    }

    _sendArgs.BufferList = _pendingList;

    bool pending = _socket.SendAsync(_sendArgs);
    if (pending == false)
        OnSendCompleted(null, _sendArgs);
}
  • _sendQueue에서 모든 패킷을 꺼내 _pendingList에 등록합니다.
  • _sendArgs.BufferList에 설정하여 Socket.SendAsync() 호출.
  • 전송이 즉시 완료되면 OnSendCompleted()가 바로 호출됩니다.

✅ 여기서도 SendBuffer의 ArraySegment가 그대로 참조 전달되므로 전송 중 추가 복사는 발생하지 않습니다.


🔷 3. OnSendCompleted()

private void OnSendCompleted(object sender, SocketAsyncEventArgs args)
{
    lock (_lock)
    {
        if (args.BytesTransferred > 0 && args.SocketError == SocketError.Success)
        {
            _sendArgs.BufferList = null;
            _pendingList.Clear();

            OnSend(_sendArgs.BytesTransferred);

            if (_sendQueue.Count > 0)
                RegisterSend();
        }
        else
        {
            Disconnect();
        }
    }
}
  • 전송이 완료되면 _pendingList 초기화 및 다음 전송 등록 여부 확인.
  • 버퍼는 이미 전송이 끝났으므로 이 시점부터는 재사용 가능하거나 GC 수거 대기 상태가 됩니다.

🔄 전체 흐름 정리 (실제 사용 예시)

Knight knight = new Knight() { hp = 100, attack = 10 };
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);

ArraySegment<byte> sendBuff = SendBufferHelper.Close(buffer.Length + buffer2.Length);
Send(sendBuff);

✅ 이 한 줄 한 줄이 모두 연결되어 있어야 합니다:

  • Open → 예약
  • 복사
  • Close → 확정
  • Send → 큐 등록
  • RegisterSend → 실제 전송 요청

🧵 멀티스레드 환경에서 안전한 이유

요소설명
TLS 구조ThreadLocal를 사용해 각 스레드가 고유한 SendBuffer를 가짐
Open/Close현재 스레드에서만 수행됨. 다른 스레드와 충돌 없음
전송 중Socket.SendAsync에 넘긴 이후는 읽기 전용 (참조만)
메모리 보호Open/Close는 쓰기, 이후는 읽기만 수행하므로 데이터 경합 없음

⚠️ 실무에서 주의할 점

  • SendBuffer는 절대로 재사용(clean)하면 안 됨
    • 아직 Socket.SendAsync 내부에서 참조 중일 수 있기 때문
  • RecvBuffer처럼 커서를 되감는 구조는 사용하지 않음
  • 사용한 버퍼는 그대로 GC에 맡기고, 필요 시 새로 할당

🔄 C++에서 개선 가능한 지점

현재 C#에서는 버퍼를 새로 만들고 GC에 맡기지만,
C++에서는 다음과 같은 방식으로 최적화할 수 있습니다:

개선점설명
레퍼런스 카운트누가 참조 중인지 확인 가능. 안전하게 재사용 여부 판단
객체 풀링SendBufferPool에서 미리 확보한 버퍼를 순환 사용
메모리 재사용필요 이상 할당 방지, 메모리 단편화 감소

✅ 핵심 요약

항목설명
SendBuffer 생성 위치반드시 Session 외부. 복사 최소화를 위해
전송 흐름Open → 복사 → Close → Send → RegisterSend
TLS 구조스레드마다 버퍼 분리, 경합 없음
참조 기반 전송복사 없이 ArraySegment로 전송
재사용 금지 이유참조 중일 가능성 존재
C++ 이식 시객체 풀링, 레퍼런스 관리 도입 고려

🧠 마무리 정리

  • SendBuffer는 단순한 바이트 배열이 아닙니다.
    복사 최소화, 멀티스레드 안전성, 송신 속도 보장을 위한 전략적 구조입니다.
  • Open()Close()를 통해 작업 공간을 미리 확보하고,
    복사 없는 참조 전달만으로 수천 개의 패킷을 안정적으로 전송할 수 있습니다.
  • 이 구조는 MMORPG 같은 대규모 서버 환경에서 필수입니다.

🎓 핵심 정리:
SendBuffer = Chunk 기반 예약 구조 + TLS 기반 독립 버퍼 + 참조 기반 전송
→ 이것이 C# 네트워크 서버 구조의 전송 최적화 핵심 해법입니다.


profile
李家네_공부방

0개의 댓글