41. 비동기 송신의 동기 완료 경로 처리 (C#)
난이도 상 해설 보기 →
결함을 모두 찾고 원인·수정안·더 나은 설계를 제시하라. 마커
(A)(B) 는 주목 위치 힌트다.
결함 코드 · C#
// [시나리오] 세션 송신 펌프 (MMORPG 게임서버, SocketAsyncEventArgs 기반)
//
// 구조:
// - 게임 로직 스레드들(존 틱, 전투, 채팅, 브로드캐스트)이 아무 때나 Send() 를 호출한다.
// - 세션마다 송신 큐를 하나 두고, 큐에서 하나씩 꺼내 비동기 송신을 건다.
// - 송신이 끝나면 다음 항목을 꺼내 다시 비동기 송신을 건다(송신 펌프).
// - 패킷 버퍼는 풀(BufferPool)에서 빌려 쓰고, 송신이 끝나면 풀로 돌려준다.
// - SocketAsyncEventArgs 는 세션당 하나만 만들어 계속 재사용한다(GC 압력 절감).
//
// [요구사항]
// - 한 세션의 패킷은 Send() 호출 순서대로, 프레임 경계가 섞이지 않게 나가야 한다.
// - 어느 스레드에서 Send() 를 호출하든 안전해야 한다.
// - 필드 보스 처치처럼 수만 개 패킷이 한꺼번에 쏟아져도 서버가 버텨야 한다.
// - 버퍼는 송신이 실제로 끝난 뒤에 반납되어야 한다.
//
// [과제]
// 아래 GameSession 을 리뷰하라. Socket.SendAsync(SocketAsyncEventArgs) 의 반환값이
// 무엇을 의미하는지 확인한 뒤, (A)(B) 지점에서 어떤 실행 경로가 가능한지
// 시나리오(로컬 루프백 클라, 대량 브로드캐스트 등)와 함께 설명하고 수정안을 제시하라.
// 콜스택 깊이·스레드 점유·버퍼 수명 관점도 함께 논하라.
// (먼저 직접 리뷰를 적은 뒤 answer.md 와 대조할 것)
using System;
using System.Collections.Generic;
using System.Net.Sockets;
public sealed class BufferPool
{
private readonly Stack<byte[]> _free = new();
private readonly object _sync = new object();
public byte[] Rent(int size)
{
lock (_sync)
{
if (_free.Count > 0) return _free.Pop();
}
return new byte[Math.Max(size, 4096)];
}
public void Return(byte[] buf)
{
lock (_sync) _free.Push(buf);
}
}
public sealed class GameSession
{
private readonly Socket _socket;
private readonly BufferPool _pool;
private readonly SocketAsyncEventArgs _sendArgs = new();
private readonly Queue<ArraySegment<byte>> _sendQueue = new();
private readonly object _queueLock = new object();
private bool _sending; // 송신 펌프가 돌고 있는지
private bool _disconnected;
public long PlayerId;
public long TotalBytesSent;
public GameSession(Socket socket, BufferPool pool)
{
_socket = socket;
_pool = pool;
_sendArgs.Completed += OnSendCompleted;
}
// 어느 스레드에서든 호출된다.
public void Send(byte[] packet, int length)
{
if (_disconnected) return;
byte[] buf = _pool.Rent(length);
Buffer.BlockCopy(packet, 0, buf, 0, length);
lock (_queueLock)
{
_sendQueue.Enqueue(new ArraySegment<byte>(buf, 0, length));
}
// (B)
if (_sending) return;
_sending = true;
PumpSend();
}
private void PumpSend()
{
ArraySegment<byte> seg;
lock (_queueLock)
{
if (_sendQueue.Count == 0)
{
// (B)
_sending = false;
return;
}
seg = _sendQueue.Dequeue();
}
// (B)
_sendArgs.SetBuffer(seg.Array, seg.Offset, seg.Count);
_sendArgs.UserToken = seg.Array;
bool pending = _socket.SendAsync(_sendArgs);
if (!pending)
{
// (A)
OnSendCompleted(this, _sendArgs);
}
}
private void OnSendCompleted(object sender, SocketAsyncEventArgs e)
{
var used = (byte[])e.UserToken;
if (e.SocketError != SocketError.Success)
{
_pool.Return(used);
Disconnect();
return;
}
TotalBytesSent += e.BytesTransferred;
_pool.Return(used);
// (A)
PumpSend();
}
private void Disconnect()
{
_disconnected = true;
try { _socket.Shutdown(SocketShutdown.Both); } catch { }
_socket.Close();
}
}
public sealed class ZoneBroadcaster
{
private readonly List<GameSession> _sessions = new();
// 필드 보스 처치 등: 존 전체에 수천 개 패킷을 한 번에 밀어넣는다.
public void BroadcastAll(List<byte[]> packets)
{
foreach (var s in _sessions)
foreach (var p in packets)
s.Send(p, p.Length);
}
} 결함 코드 · C++
// ============================================================================
// [코드리뷰 문제] C++ - 세션 송신 펌프와 비동기 send 의 완료 경로
// ----------------------------------------------------------------------------
// 시나리오 (세션/네트워크 · 서버):
// - 게임 로직 스레드들(존 틱, 전투, 채팅, 브로드캐스트)이 아무 때나 Session::send() 를
// 호출한다. 세션마다 송신 큐를 하나 두고, 큐에서 하나씩 꺼내 비동기 송신을 건다.
// - 송신이 끝나면 다음 항목을 꺼내 다시 비동기 송신을 건다(송신 펌프).
// - 송신 버퍼는 풀에서 빌려 쓰고, 송신이 끝나면 풀로 돌려준다.
// - OVERLAPPED 구조체는 세션당 하나만 만들어 계속 재사용한다.
//
// * 이 파일의 AsyncSocket 은 Win32 IOCP 의 WSASend 동작(즉시 완료 / 보류)을 모사한
// 인터페이스다. 리눅스 g++ 로도 컴파일되도록 POSIX/표준 C++ 만 사용한다.
// - send() 가 SendResult::Completed 를 돌려주면 "즉시 완료"(WSASend 가 0 반환)
// - SendResult::Pending 이면 "보류"(WSA_IO_PENDING)
// 완료 통지(completion handler)가 각각 언제 올라오는지는 AsyncSocket 주석을 볼 것.
//
// [요구사항]
// - 한 세션의 패킷은 send() 호출 순서대로, 프레임 경계가 섞이지 않게 나가야 한다.
// - 어느 스레드에서 send() 를 호출하든 안전해야 한다.
// - 필드 보스 처치처럼 수만 개 패킷이 한꺼번에 쏟아져도 서버가 버텨야 한다.
// - 버퍼는 송신이 실제로 끝난 뒤에 반납되어야 한다.
//
// 과제:
// 아래 Session 을 리뷰하라. (A)(B) 지점에서 어떤 실행 경로가 가능한지
// 시나리오(로컬 루프백 클라, 대량 브로드캐스트 등)와 함께 설명하고,
// 콜스택 깊이·스레드 점유·버퍼 수명 관점까지 논한 뒤 수정안과 더 나은 설계를 제시하라.
// (먼저 직접 리뷰를 적은 뒤 answer.md 와 대조할 것)
// ============================================================================
#include <atomic>
#include <cstddef>
#include <cstdint>
#include <cstring>
#include <deque>
#include <functional>
#include <mutex>
#include <vector>
// ---------------------------------------------------------------- 모사 계층
struct Overlapped {
const char* buffer = nullptr;
std::size_t length = 0;
void* owner = nullptr;
};
enum class SendResult { Completed, Pending, Error };
class AsyncSocket {
public:
using CompletionHandler = std::function<void(Overlapped*, std::size_t)>;
explicit AsyncSocket(int fd) : fd_(fd) {}
void setCompletionHandler(CompletionHandler h) { handler_ = std::move(h); }
// IOCP 의 WSASend 를 모사한다.
// - 커널 송신 여유에 즉시 다 들어가면 Completed 를 돌려주고 bytesSent 를 채운다.
// 이 경우에도 완료 통지는 완료 포트를 통해 별도로 한 번 더 올라온다.
// - 여유가 없으면 Pending 을 돌려주고, 나중에 완료 통지가 올라온다.
SendResult send(Overlapped* ov, const char* buf, std::size_t len, std::size_t& bytesSent);
// 완료 포트 워커 스레드가 통지를 꺼내 이 함수를 호출한다.
void dispatchCompletion(Overlapped* ov, std::size_t bytes) {
if (handler_) handler_(ov, bytes);
}
private:
int fd_;
CompletionHandler handler_;
std::atomic<std::size_t> kernelFree_{64 * 1024};
};
class BufferPool {
public:
char* rent(std::size_t n) {
std::lock_guard<std::mutex> lk(mtx_);
if (!free_.empty()) { char* p = free_.back(); free_.pop_back(); return p; }
return new char[n < 4096 ? 4096 : n];
}
void release(char* p) {
std::lock_guard<std::mutex> lk(mtx_);
free_.push_back(p);
}
private:
std::mutex mtx_;
std::vector<char*> free_;
};
// ---------------------------------------------------------------- 세션
struct SendItem {
char* data = nullptr;
std::size_t len = 0;
};
class Session {
public:
Session(AsyncSocket* sock, BufferPool* pool) : sock_(sock), pool_(pool) {
sock_->setCompletionHandler(
[this](Overlapped* ov, std::size_t bytes) { onSendCompleted(ov, bytes); });
}
// 어느 스레드에서든 호출된다.
void send(const char* packet, std::size_t len) {
if (disconnected_) return;
char* buf = pool_->rent(len);
std::memcpy(buf, packet, len);
{
std::lock_guard<std::mutex> lk(mtx_);
queue_.push_back(SendItem{buf, len});
}
// (B)
if (sending_) return;
sending_ = true;
pumpSend();
}
private:
void pumpSend() {
SendItem item;
{
std::lock_guard<std::mutex> lk(mtx_);
if (queue_.empty()) {
// (B)
sending_ = false;
return;
}
item = queue_.front();
queue_.pop_front();
}
// (B)
ov_.buffer = item.data;
ov_.length = item.len;
ov_.owner = this;
std::size_t sent = 0;
SendResult r = sock_->send(&ov_, item.data, item.len, sent);
if (r == SendResult::Error) {
pool_->release(item.data);
disconnect();
return;
}
// (B)
pool_->release(item.data);
if (r == SendResult::Completed) {
// (A)
onSendCompleted(&ov_, sent);
}
}
void onSendCompleted(Overlapped* ov, std::size_t bytes) {
(void)ov;
totalSent_ += bytes;
// (A)
pumpSend();
}
void disconnect() { disconnected_ = true; }
AsyncSocket* sock_;
BufferPool* pool_;
std::mutex mtx_;
std::deque<SendItem> queue_;
Overlapped ov_;
bool sending_ = false;
bool disconnected_ = false;
std::uint64_t totalSent_ = 0;
}; 내 리뷰 · C#
내 답안 · 자동 저장
작성 후 위 해설 보기에서 모범 해설과 대조하세요.
내 리뷰 · C++
내 답안 · 자동 저장
작성 후 위 해설 보기에서 모범 해설과 대조하세요.