수업

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

// offset이 변한다는 개념
namespace ServerCore
{
    // 10바이트 배열이라고 가정하고 시작
    // [r/w][][][][][][][][][] : _readPos, _writePos 처음에 있고 나머지가 비어있음 시작 하는 부분
    // [r/][][][][][w][][][][] : 5바이트
    // [][][][][][][][r/w][][] : 전송 완료 후 다시 초기화
    // [][][r/][w][][][] : 이상태에서 대기 // 각 2바이트 경우
    // 다시 처음으로 옮김
    // 유효 범위를 바꿔줌
    // [r][w][][][][][]
    public class RecvBuffer
    {
        ArraySegment<byte> _buffer;

        int _readPos;
        int _writePos;
        public RecvBuffer(int bufferSize)
        {
            _buffer = new ArraySegment<byte>(new byte[bufferSize], 0, bufferSize);
        }

        // 버퍼에 있는 데이터 크기
        public int DataSize { get { return _writePos - _readPos; } }

        // 버퍼의 남은 공간
        public int FreeSize { get { return _buffer.Count - _writePos; } }

        // 유효범위 세크먼트
        // 읽을 수 있는 데이터 범위 r - w 범위
        public ArraySegment<byte> ReadSegment
        {
            get { return new ArraySegment<byte>(_buffer.Array, _buffer.Offset + _readPos, DataSize); }
        }

        // 데이터를 새로 받을 공간을 반환함
        public ArraySegment<byte> WriteSegment 
        {
            get { return new ArraySegment<byte>(_buffer.Array, _buffer.Offset + _writePos, FreeSize); }
        }

        // 중간 중간 저리
        public void Clean()
        {
            int dataSize = DataSize;

            // 커서 위치를 처음으로 바꿈
            // 남은 데이터가 없으면 커서 위치만 바꾸는 경우임
            if (dataSize == 0)
            {
                _readPos = _writePos = 0;
            }
            // 남은 데이터가 있는 경우
            // 시작 위치로 복사해야한다는 의미
            else
            {
                Array.Copy(_buffer.Array, _buffer.Offset + _readPos, _buffer.Array, _buffer.Offset, dataSize);
                _readPos = 0;
                _writePos = dataSize;
            }
        }

        // 데이터를 성공적으로 처리후 커서위치 이동
        public bool OnRead(int numOfBytes)
        {
            if (numOfBytes > DataSize)
                return false;

            _readPos += numOfBytes;
            return true;
        }

        // 데이터 쓰기 완료 후 WritePos 이동
        public bool OnWrite(int numOfBytes)
        {
            if (numOfBytes > FreeSize)
                return false;

            _writePos += numOfBytes;
            return true;
        }
    }
}

---

namespace Server
{
    public class GameSession : Session
    {
        public override void OnConnected(EndPoint endPoint)
        {
            Console.WriteLine($"OnConnected: {endPoint}");
            byte[] sendBuff = Encoding.UTF8.GetBytes("Welcome to MMORPG Server!");
            Send(sendBuff);
            Thread.Sleep(1000);
            Disconnect();
        }

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

        public override int OnRecv(ArraySegment<byte> buffer)
        {
            string recvData = Encoding.UTF8.GetString(buffer.Array, buffer.Offset, buffer.Count);
            Console.WriteLine($"[From Client] {recvData}");

            return buffer.Count;
        }

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

---

namespace DummyClient
{
    class GameSession : Session
    {
        public override void OnConnected(EndPoint endPoint)
        {
            Console.WriteLine($"OnConnected: {endPoint}");

            // 서버에 5번 메시지를 전송
            for (int i = 0; i < 5; i++)
            {
                byte[] sendBuff = Encoding.UTF8.GetBytes($"Hello World! {i}");
                Send(sendBuff);
            }
        }

        public override void OnDisconnected(EndPoint endPoint)
        {
            Console.WriteLine($"OnDisconnected: {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}");
        }
    }
}

좋습니다. 지금부터는 여러분이 제공해주신 “RecvBuffer” 강의 블로그 분석글들을 완전히 통합해서, 강사 스타일의 학습 교재 형식으로 재작성해드리겠습니다.
전체 내용이 방대하므로 총 2부 구성으로 나누어 설명드리며,
이번은 1부로, RecvBuffer의 설계 목적, 커서 기반 구조, 생성자, 프로퍼티, Segment 처리 방식까지 설명합니다.


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


✅ 주제

  • 이 강의의 핵심 주제는 TCP 스트림 기반 통신 환경에서, 수신 데이터의 누적, 관리, 커서 기반 처리 구조를 RecvBuffer 클래스로 어떻게 구현하는가입니다.
  • 특히 비동기 소켓 기반 네트워크 프로그래밍에서, 부분 수신, 유효 데이터 범위 관리, 데이터 조립 및 메모리 회전 처리가 주요 학습 포인트입니다.

📚 개념

RecvBuffer가 필요한 이유

TCP는 데이터의 순서를 보장하고 신뢰성 있는 전송을 보장하지만,
패킷 단위가 아닌 스트림 기반 전송입니다.
즉, 100바이트를 보냈다고 해서 반드시 한 번에 100바이트가 오는 것이 아니라,
예를 들어 80바이트만 먼저 오고, 20바이트가 나중에 도착할 수 있습니다.

그렇기 때문에 우리는 수신 데이터 전체를 임시 보관하고,
완전한 패킷이 도착할 때까지 유효 범위를 관리하면서
필요 시 조립하고 처리할 수 있어야 합니다.

이때 사용하는 구조가 바로 RecvBuffer입니다.


📘 용어 정리

용어설명
RecvBufferTCP 수신 데이터를 저장하고, 유효 범위를 관리하며, 커서 기반으로 조립하는 클래스
ArraySegment배열의 특정 범위를 가리킬 수 있도록 설계된 구조체. 효율적인 데이터 처리에 활용
ReadPos (_readPos)읽기 시작 커서 위치. 아직 처리되지 않은 데이터의 시작점
WritePos (_writePos)쓰기 커서 위치. 마지막으로 데이터를 수신한 지점
DataSize_writePos - _readPos 읽을 수 있는 데이터 크기
FreeSize_buffer.Count - _writePos 현재 남아 있는 여유 공간
ReadSegment읽을 수 있는 데이터 범위. 컨텐츠 코드에 넘길 범위
WriteSegment데이터를 수신받을 수 있는 범위
Clean()사용한 버퍼 영역을 당겨 커서를 정리하고 공간을 재활용
OnRead()처리한 만큼 읽기 커서를 이동
OnWrite()수신한 만큼 쓰기 커서를 이동

💻 코드 분석

🔷 1. 클래스 선언 및 필드

public class RecvBuffer
{
    ArraySegment<byte> _buffer;
    int _readPos;
    int _writePos;
  • _buffer: 실제 데이터를 저장하는 메모리 영역입니다. ArraySegment<byte> 타입으로 관리됩니다.
  • _readPos: 데이터를 읽기 시작할 커서 위치입니다.
  • _writePos: 데이터를 수신해 기록할 커서 위치입니다.

📌 이 두 커서를 기준으로 현재 읽을 수 있는 범위와 앞으로 쓸 수 있는 공간을 계산합니다.


🔷 2. 생성자

public RecvBuffer(int bufferSize)
{
    _buffer = new ArraySegment<byte>(new byte[bufferSize], 0, bufferSize);
}
  • 생성자는 bufferSize 크기의 버퍼를 생성하며, 해당 크기를 ArraySegment로 감싸 저장합니다.
  • 이 구조 덕분에 고정된 메모리 내에서 버퍼를 재활용할 수 있으며, 전체 배열이 아닌 부분만 안전하게 다룰 수 있습니다.

💡 ArraySegment는 배열 일부를 참조하여, 복사 비용 없이 포인터처럼 범위를 지정할 수 있는 구조입니다.


🔷 3. 유효 데이터 크기와 남은 공간

public int DataSize { get { return _writePos - _readPos; } }
public int FreeSize { get { return _buffer.Count - _writePos; } }
  • DataSize: 현재 읽을 수 있는 데이터의 크기입니다.
    _writePos까지 데이터를 수신한 상태이며, 아직 _readPos까지밖에 처리하지 않았기 때문입니다.
  • FreeSize: 현재 버퍼에서 앞으로 더 수신할 수 있는 공간의 크기입니다.

✅ 이 두 값은 각각 컨텐츠 코드에서 처리 가능한 양
✅ 다음 수신 시 얼마나 공간이 있는지를 판단하는 데 사용됩니다.


🔷 4. ReadSegment / WriteSegment

public ArraySegment<byte> ReadSegment
{
    get { return new ArraySegment<byte>(_buffer.Array, _buffer.Offset + _readPos, DataSize); }
}

public ArraySegment<byte> WriteSegment
{
    get { return new ArraySegment<byte>(_buffer.Array, _buffer.Offset + _writePos, FreeSize); }
}

📌 ReadSegment

  • 아직 처리되지 않은 유효 데이터를 가리킵니다.
  • 실제로 OnRecv()에 넘겨주는 데이터는 이 범위입니다.

📌 WriteSegment

  • 앞으로 수신할 데이터를 저장할 수 있는 공간입니다.
  • ReceiveAsync() 호출 시 이 공간에 데이터를 받아옵니다.

📌 RecvBuffer는 단순한 배열이 아닌
유효 범위만을 안전하게 다루는 인터페이스를 외부에 제공합니다.


📌 예시 커서 흐름 예제

초기 상태:      [r w][ ][ ][ ][ ][ ][ ][ ][ ][ ]
5바이트 수신:    [r][ ][ ][ ][ ][w][ ][ ][ ][ ]
2바이트 처리:    [ ][ ][r][ ][ ][w][ ][ ][ ][ ]
정리 후 상태:    [r w][ ][ ][ ][ ][ ][ ][ ][ ][ ]
  • _readPos는 컨텐츠 코드가 데이터를 처리할 때 이동됩니다.
  • _writePos는 Receive로 데이터를 수신할 때 이동됩니다.
  • 중간 중간 Clean()을 호출하여 버퍼 공간을 앞쪽으로 당겨 재활용합니다.

🧠 2부 예고

다음 2부에서는

  • Clean() 메서드의 내부 처리 흐름
  • OnRead(), OnWrite()의 방어 로직
  • 그리고 Session 클래스에서 RecvBuffer가 어떻게 실제로 연결되고
  • RegisterRecv() / OnRecvCompleted() 흐름 안에서 RecvBuffer가 어떤 식으로 커서를 이동시키는지를
    완벽하게 분석해드리겠습니다.

👉 이어서 2부 작성하겠습니다.

좋습니다. 이전 1부에서는 RecvBuffer 클래스의 설계 목적, 커서 기반 수신 구조, 데이터 세그먼트 관리, 그리고 Clean/OnRead/OnWrite의 내부 메커니즘까지 자세히 학습했습니다.
이제 2부에서는 이 RecvBuffer가 실제로 Session 클래스와 어떻게 연결되어 동작하며, 수신 데이터가 어떻게 흘러가고, RecvBuffer가 어디서 어떤 역할을 수행하는지를 전체적인 수신 흐름 관점에서 살펴보겠습니다.


🎓 RecvBuffer 완전 강의 교재 (2부 / 총 2부)


✅ 주제

  • RecvBuffer를 실제 Session 클래스에 어떻게 연동하는가?
  • TCP 환경에서 RecvBuffer가 비동기 수신 흐름 중 어떤 위치에서 어떤 역할을 수행하는가?
  • 커서 이동, Clean, 처리 범위 지정 등 실전 연동 시나리오 전체 흐름 학습

📚 개념 요약

RecvBuffer의 책임

  1. 부분 수신 데이터를 안전하게 누적
  2. 누적된 데이터를 읽을 수 있는 범위(ReadSegment)로 구분
  3. 처리가 완료된 데이터는 커서를 이동(OnRead)
  4. 새로 수신할 공간은 WriteSegment로 제공
  5. 커서가 너무 뒤로 밀리면 Clean으로 정리

📘 용어정리

용어설명
RegisterRecv()다음 ReceiveAsync 호출 시 수신 받을 버퍼 영역을 지정
OnRecvCompleted()비동기 수신 완료 후 실행되는 콜백 메서드
OnWrite(numOfBytes)수신된 데이터 크기만큼 Write 커서를 이동
ReadSegment수신된 데이터 중 아직 처리하지 않은 범위
OnRecv(ReadSegment)유효 범위 데이터를 컨텐츠 코드에 넘김
OnRead(numOfBytes)처리 완료된 데이터만큼 Read 커서 이동
Clean()버퍼 밀림 방지를 위한 정리 함수

💻 코드 분석 — Session과 RecvBuffer 연동 흐름

1️⃣ RecvBuffer 선언 위치

RecvBuffer _recvBuffer = new RecvBuffer(1024);
  • Session 클래스 내부에 포함되어 있으며, 한 세션당 하나씩 관리
  • 1024 크기의 고정 버퍼 생성
  • 이 버퍼는 연결이 유지되는 동안 지속적으로 사용

2️⃣ RegisterRecv()

void RegisterRecv()
{
    _recvBuffer.Clean(); // 커서 위치가 너무 뒤로 가는 것을 방지하기 위해 버퍼 정리
    ArraySegment<byte> segment = _recvBuffer.WriteSegment; // 현재 받을 수 있는 공간 추출
    _recvArgs.SetBuffer(segment.Array, segment.Offset, segment.Count); // 수신 대상 버퍼 설정

    bool pending = _socket.ReceiveAsync(_recvArgs);
    if (pending == false)
        OnRecvCompleted(null, _recvArgs);
}

🔍 여기서 핵심은:

  • Clean() 호출 → r/w 커서 정리
  • WriteSegment 추출 → 수신 가능 공간 지정
  • SetBuffer로 지정 → 이후 수신할 위치 설정

즉, 다음 수신은 WriteSegment에 저장되도록 설정하는 절차입니다.


3️⃣ OnRecvCompleted()

void OnRecvCompleted(object sender, SocketAsyncEventArgs args)
{
    if (args.BytesTransferred > 0 && args.SocketError == SocketError.Success)
    {
        try
        {
            if (_recvBuffer.OnWrite(args.BytesTransferred) == false)
            {
                Disconnect();
                return;
            }

            int processLen = OnRecv(_recvBuffer.ReadSegment);

            if (processLen < 0 || _recvBuffer.DataSize < processLen)
            {
                Disconnect();
                return;
            }

            if (_recvBuffer.OnRead(processLen) == false)
            {
                Disconnect();
                return;
            }

            RegisterRecv(); // 다시 다음 수신 대기
        }
        catch (Exception e)
        {
            Console.WriteLine($"OnRecvCompleted Failed: {e}");
        }
    }
    else
    {
        Disconnect();
    }
}

💡 이 메서드는 RecvBuffer의 정식 수명주기를 수행합니다.

단계설명
1OnWrite(args.BytesTransferred) — 쓰기 커서 이동
2ReadSegment → OnRecv(ReadSegment) — 유효 범위 처리 요청
3OnRead(processLen) — 처리된 만큼 읽기 커서 이동
4RegisterRecv() — 다음 ReceiveAsync 호출 준비

🧪 실전 흐름 예제 — 수신 흐름 전체 요약

Client에서 100바이트 데이터 전송
 └▶ ReceiveAsync(WriteSegment) 호출
    └▶ 실제 수신은 60바이트 도착
       └▶ OnWrite(60) → Write 커서 이동
       └▶ ReadSegment 전달 → OnRecv 호출
          └▶ processLen = 40
             └▶ OnRead(40) → Read 커서 이동
 └▶ 다음 Recv 등록
    └▶ Clean() 호출 → 남은 20바이트 앞으로 복사
    └▶ WriteSegment(뒤 공간) 지정 후 ReceiveAsync 호출

🔁 커서 이동 시나리오 예시

초기 상태:    [r w][][][][][][][][][]
데이터 수신 후: [r][][][][][w][][][][]
데이터 일부 처리: [][][r][][][w][][][]
Clean 호출 후: [r w][][][][][][][][][]
  • 이 구조가 유지되므로 무한히 데이터를 수신/조립/처리할 수 있게 됩니다.

🧠 핵심 정리

항목기능
RecvBuffer커서 기반 수신 구조로 TCP 조립에 특화
RegisterRecv수신할 WriteSegment를 지정
OnRecvCompleted수신 완료 후 Write → Read 처리 흐름 진행
OnRecv()실제 컨텐츠 처리, 처리량 반환
Clean()커서 밀림 방지 및 공간 재활용

✅ 최종 마무리: RecvBuffer의 진짜 목적

RecvBuffer는 단순히 데이터를 저장하는 구조가 아닙니다.
TCP 스트림 기반 수신 처리의 핵심 설계 원칙을 반영한 구조입니다.

  • 비동기 수신 흐름에 최적화되어 있으며,
  • 패킷 단위 처리 이전 단계에서 안정적인 데이터 조립 환경을 보장하고,
  • 컨텐츠 단에서는 OnRecv → 처리량 반환 → Read 커서 이동이라는 일관된 패턴으로 처리합니다.

✅ 이 구조를 완전히 이해하고 있어야,
이후 패킷 조립기, 헤더 분석기, 패킷 큐 설계까지 확장할 수 있습니다.


이상으로 RecvBuffer 완전 통합 강의 교재 2부를 마칩니다.
다음 단계로 넘어가고 싶으시면, Listener / Connector / Session 확장 구조에 대한 연계 설명도 이어서 작성해드릴 수 있습니다.

profile
李家네_공부방

0개의 댓글