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 처리 방식까지 설명합니다.
TCP는 데이터의 순서를 보장하고 신뢰성 있는 전송을 보장하지만,
패킷 단위가 아닌 스트림 기반 전송입니다.
즉, 100바이트를 보냈다고 해서 반드시 한 번에 100바이트가 오는 것이 아니라,
예를 들어 80바이트만 먼저 오고, 20바이트가 나중에 도착할 수 있습니다.
그렇기 때문에 우리는 수신 데이터 전체를 임시 보관하고,
완전한 패킷이 도착할 때까지 유효 범위를 관리하면서
필요 시 조립하고 처리할 수 있어야 합니다.
이때 사용하는 구조가 바로 RecvBuffer입니다.
| 용어 | 설명 |
|---|---|
| RecvBuffer | TCP 수신 데이터를 저장하고, 유효 범위를 관리하며, 커서 기반으로 조립하는 클래스 |
| ArraySegment | 배열의 특정 범위를 가리킬 수 있도록 설계된 구조체. 효율적인 데이터 처리에 활용 |
| ReadPos (_readPos) | 읽기 시작 커서 위치. 아직 처리되지 않은 데이터의 시작점 |
| WritePos (_writePos) | 쓰기 커서 위치. 마지막으로 데이터를 수신한 지점 |
| DataSize | _writePos - _readPos 읽을 수 있는 데이터 크기 |
| FreeSize | _buffer.Count - _writePos 현재 남아 있는 여유 공간 |
| ReadSegment | 읽을 수 있는 데이터 범위. 컨텐츠 코드에 넘길 범위 |
| WriteSegment | 데이터를 수신받을 수 있는 범위 |
| Clean() | 사용한 버퍼 영역을 당겨 커서를 정리하고 공간을 재활용 |
| OnRead() | 처리한 만큼 읽기 커서를 이동 |
| OnWrite() | 수신한 만큼 쓰기 커서를 이동 |
public class RecvBuffer
{
ArraySegment<byte> _buffer;
int _readPos;
int _writePos;
_buffer: 실제 데이터를 저장하는 메모리 영역입니다. ArraySegment<byte> 타입으로 관리됩니다._readPos: 데이터를 읽기 시작할 커서 위치입니다._writePos: 데이터를 수신해 기록할 커서 위치입니다.📌 이 두 커서를 기준으로 현재 읽을 수 있는 범위와 앞으로 쓸 수 있는 공간을 계산합니다.
public RecvBuffer(int bufferSize)
{
_buffer = new ArraySegment<byte>(new byte[bufferSize], 0, bufferSize);
}
bufferSize 크기의 버퍼를 생성하며, 해당 크기를 ArraySegment로 감싸 저장합니다.💡 ArraySegment는 배열 일부를 참조하여, 복사 비용 없이 포인터처럼 범위를 지정할 수 있는 구조입니다.
public int DataSize { get { return _writePos - _readPos; } }
public int FreeSize { get { return _buffer.Count - _writePos; } }
DataSize: 현재 읽을 수 있는 데이터의 크기입니다._writePos까지 데이터를 수신한 상태이며, 아직 _readPos까지밖에 처리하지 않았기 때문입니다.FreeSize: 현재 버퍼에서 앞으로 더 수신할 수 있는 공간의 크기입니다.✅ 이 두 값은 각각 컨텐츠 코드에서 처리 가능한 양과
✅ 다음 수신 시 얼마나 공간이 있는지를 판단하는 데 사용됩니다.
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); }
}
OnRecv()에 넘겨주는 데이터는 이 범위입니다.ReceiveAsync() 호출 시 이 공간에 데이터를 받아옵니다.📌 RecvBuffer는 단순한 배열이 아닌
유효 범위만을 안전하게 다루는 인터페이스를 외부에 제공합니다.
초기 상태: [r w][ ][ ][ ][ ][ ][ ][ ][ ][ ]
5바이트 수신: [r][ ][ ][ ][ ][w][ ][ ][ ][ ]
2바이트 처리: [ ][ ][r][ ][ ][w][ ][ ][ ][ ]
정리 후 상태: [r w][ ][ ][ ][ ][ ][ ][ ][ ][ ]
_readPos는 컨텐츠 코드가 데이터를 처리할 때 이동됩니다._writePos는 Receive로 데이터를 수신할 때 이동됩니다.Clean()을 호출하여 버퍼 공간을 앞쪽으로 당겨 재활용합니다.다음 2부에서는
Clean() 메서드의 내부 처리 흐름 OnRead(), OnWrite()의 방어 로직 Session 클래스에서 RecvBuffer가 어떻게 실제로 연결되고 RegisterRecv() / OnRecvCompleted() 흐름 안에서 RecvBuffer가 어떤 식으로 커서를 이동시키는지를👉 이어서 2부 작성하겠습니다.
좋습니다. 이전 1부에서는 RecvBuffer 클래스의 설계 목적, 커서 기반 수신 구조, 데이터 세그먼트 관리, 그리고 Clean/OnRead/OnWrite의 내부 메커니즘까지 자세히 학습했습니다.
이제 2부에서는 이 RecvBuffer가 실제로 Session 클래스와 어떻게 연결되어 동작하며, 수신 데이터가 어떻게 흘러가고, RecvBuffer가 어디서 어떤 역할을 수행하는지를 전체적인 수신 흐름 관점에서 살펴보겠습니다.
| 용어 | 설명 |
|---|---|
| RegisterRecv() | 다음 ReceiveAsync 호출 시 수신 받을 버퍼 영역을 지정 |
| OnRecvCompleted() | 비동기 수신 완료 후 실행되는 콜백 메서드 |
| OnWrite(numOfBytes) | 수신된 데이터 크기만큼 Write 커서를 이동 |
| ReadSegment | 수신된 데이터 중 아직 처리하지 않은 범위 |
| OnRecv(ReadSegment) | 유효 범위 데이터를 컨텐츠 코드에 넘김 |
| OnRead(numOfBytes) | 처리 완료된 데이터만큼 Read 커서 이동 |
| Clean() | 버퍼 밀림 방지를 위한 정리 함수 |
RecvBuffer _recvBuffer = new RecvBuffer(1024);
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);
}
🔍 여기서 핵심은:
즉, 다음 수신은 WriteSegment에 저장되도록 설정하는 절차입니다.
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의 정식 수명주기를 수행합니다.
| 단계 | 설명 |
|---|---|
| 1 | OnWrite(args.BytesTransferred) — 쓰기 커서 이동 |
| 2 | ReadSegment → OnRecv(ReadSegment) — 유효 범위 처리 요청 |
| 3 | OnRead(processLen) — 처리된 만큼 읽기 커서 이동 |
| 4 | RegisterRecv() — 다음 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는 단순히 데이터를 저장하는 구조가 아닙니다.
TCP 스트림 기반 수신 처리의 핵심 설계 원칙을 반영한 구조입니다.
✅ 이 구조를 완전히 이해하고 있어야,
이후 패킷 조립기, 헤더 분석기, 패킷 큐 설계까지 확장할 수 있습니다.
이상으로 RecvBuffer 완전 통합 강의 교재 2부를 마칩니다.
다음 단계로 넘어가고 싶으시면, Listener / Connector / Session 확장 구조에 대한 연계 설명도 이어서 작성해드릴 수 있습니다.