5. C# 송신 큐 백프레셔 + 끊김/재접속 세션 복구 (복합)
난이도 최상 해설 보기 →
결함을 모두 찾고 원인·수정안·더 나은 설계를 제시하라. 마커
(A)(B) 는 주목 위치 힌트다.
결함 코드 · C#
// ============================================================================
// [코드리뷰 문제] C# - 송신 큐 백프레셔 + 끊김/재접속 세션 복구 (복합)
// ----------------------------------------------------------------------------
// 시나리오 (실서비스 MMO 게이트웨이):
// - 각 세션은 송신 큐를 두고, 단일 비동기 송신 루프가 큐를 비운다.
// (소켓에 동시에 둘 이상 WriteAsync 하면 안 되므로 직렬화한다.)
// - 일부 클라는 모바일/해외라 느리다(slow consumer). 서버 브로드캐스트는
// 초당 수십~수백 패킷을 모든 세션에 Enqueue 한다.
// - 연결이 끊기면 세션을 즉시 버리지 않고 ResumeWindow(예: 30초) 동안
// 보관한다. 같은 토큰으로 재접속하면 기존 세션 상태와 "보내다 만" 패킷을
// 이어서 보낸다(seamless reconnect).
// - Enqueue 는 게임 로직 스레드들에서, 송신 루프는 IO, 끊김/재접속은 또 다른
// 스레드에서 동시에 일어난다.
//
// 요구사항:
// - 느린 클라 하나가 서버 전체 메모리를 끌어내리면 안 된다(백프레셔/흐름 제어).
// - 재접속 시 패킷 유실/중복/순서 뒤바뀜이 없어야 한다(정합성).
// - 끊김 중 쌓인 큐가 무한히 자라거나 영원히 남으면 안 된다.
//
// 과제:
// 이 코드의 잠재적 문제를 모두 찾고, 왜 문제인지 설명하고,
// 수정안과 더 나은 설계를 제시하라.
// (먼저 직접 리뷰를 적은 뒤 answer.md 와 대조할 것)
// ============================================================================
using System;
using System.Collections.Generic;
using System.Net.Sockets;
using System.Threading;
using System.Threading.Tasks;
public sealed class GameSession
{
public string ResumeToken;
private Socket _socket;
private readonly Queue<byte[]> _sendQueue = new Queue<byte[]>(); // (A)
private bool _sending = false; // (B)
public DateTime DisconnectedAt;
public bool Connected = true;
public GameSession(Socket s) { _socket = s; }
// 게임 로직 스레드들에서 호출 (브로드캐스트 등)
public void Enqueue(byte[] packet)
{
lock (_sendQueue)
{
_sendQueue.Enqueue(packet); // (A)
if (!_sending)
{
_sending = true;
_ = SendLoopAsync(); // (B)
}
}
}
private async Task SendLoopAsync()
{
while (true)
{
byte[] next;
lock (_sendQueue)
{
if (_sendQueue.Count == 0) { _sending = false; return; }
next = _sendQueue.Dequeue();
}
// (C) 느린 클라면 여기서 오래 블록될 수 있음
await _socket.SendAsync(next, SocketFlags.None);
}
}
// 끊김 시 호출
public void OnDisconnect()
{
Connected = false;
DisconnectedAt = DateTime.UtcNow;
// 큐는 그대로 둔다(재접속 시 이어 보내려고). (D)
}
// 재접속 시 호출: 새 소켓으로 교체하고 큐를 이어서 보낸다
public void Resume(Socket newSocket)
{
_socket = newSocket; // (E) 소켓 교체
Connected = true;
if (!_sending)
{
_sending = true;
_ = SendLoopAsync(); // 남은 큐 이어 보내기
}
}
}
public sealed class SessionRegistry
{
private readonly Dictionary<string, GameSession> _byToken =
new Dictionary<string, GameSession>();
public GameSession FindForResume(string token)
{
// (F) 만료(ResumeWindow) 검사/정리 로직이 없다
_byToken.TryGetValue(token, out var s);
return s;
}
public void Register(GameSession s) => _byToken[s.ResumeToken] = s;
} 결함 코드 · C++
// ============================================================================
// [코드리뷰 문제] C++ - 송신 큐 백프레셔 + 끊김/재접속 세션 복구 (복합)
// ----------------------------------------------------------------------------
// 시나리오 (실서비스 MMO 게이트웨이):
// - 각 세션은 송신 큐를 두고, 단일 송신 스레드가 큐를 비운다.
// (한 소켓에 동시에 둘 이상 send 하면 안 되므로 직렬화한다.)
// - 일부 클라는 모바일/해외라 느리다(slow consumer). 서버 브로드캐스트는
// 초당 수십~수백 패킷을 모든 세션에 Enqueue 한다.
// - 연결이 끊기면 세션을 즉시 버리지 않고 ResumeWindow(예: 30초) 동안
// 보관한다. 같은 토큰으로 재접속하면 기존 세션 상태와 "보내다 만" 패킷을
// 이어서 보낸다(seamless reconnect).
// - Enqueue 는 게임 로직 스레드들에서, 송신 루프는 별도 스레드, 끊김/재접속은
// 또 다른 스레드에서 동시에 일어난다.
//
// 요구사항:
// - 느린 클라 하나가 서버 전체 메모리를 끌어내리면 안 된다(백프레셔/흐름 제어).
// - 재접속 시 패킷 유실/중복/순서 뒤바뀜이 없어야 한다(정합성).
// - 끊김 중 쌓인 큐가 무한히 자라거나 영원히 남으면 안 된다.
//
// 과제:
// 이 코드의 잠재적 문제를 모두 찾고, 왜 문제인지 설명하고,
// 수정안과 더 나은 설계를 제시하라.
// (먼저 직접 리뷰를 적은 뒤 answer.md 와 대조할 것)
// ============================================================================
#include <queue>
#include <vector>
#include <mutex>
#include <thread>
#include <unordered_map>
#include <string>
#include <chrono>
#include <cstdint>
// 단순 소켓 래퍼(생략): Send 는 느린 클라면 오래 블록될 수 있음
struct Socket {
int fd;
int Send(const std::vector<char>& data) { /* blocking send ... */ return (int)data.size(); }
};
class GameSession {
public:
std::string resumeToken;
private:
Socket* socket_; // (E) raw 소켓 포인터
std::queue<std::vector<char>> sendQueue_; // (A) 무제한 큐
std::mutex queueMutex_;
bool sending_ = false; // (B)
std::chrono::steady_clock::time_point disconnectedAt_;
bool connected_ = true;
public:
explicit GameSession(Socket* s) : socket_(s) {}
// 게임 로직 스레드들에서 호출 (브로드캐스트 등)
void Enqueue(std::vector<char> packet) {
std::lock_guard<std::mutex> lk(queueMutex_);
sendQueue_.push(std::move(packet)); // (A)
if (!sending_) {
sending_ = true;
std::thread([this] { SendLoop(); }).detach(); // (B)
}
}
void SendLoop() {
while (true) {
std::vector<char> next;
{
std::lock_guard<std::mutex> lk(queueMutex_);
if (sendQueue_.empty()) { sending_ = false; return; }
next = std::move(sendQueue_.front());
sendQueue_.pop();
}
// (C) 느린 클라면 여기서 오래 블록될 수 있음. 부분 전송 처리도 없음.
socket_->Send(next);
}
}
// 끊김 시 호출
void OnDisconnect() {
connected_ = false;
disconnectedAt_ = std::chrono::steady_clock::now();
// 큐는 그대로 둔다(재접속 시 이어 보내려고). (D)
}
// 재접속 시 호출: 새 소켓으로 교체하고 큐를 이어서 보낸다
void Resume(Socket* newSocket) {
socket_ = newSocket; // (E) 소켓 교체(락 없음)
connected_ = true;
if (!sending_) { // (B) 락 없이 플래그 검사
sending_ = true;
std::thread([this] { SendLoop(); }).detach(); // 남은 큐 이어 보내기
}
}
};
class SessionRegistry {
std::unordered_map<std::string, GameSession*> byToken_;
public:
GameSession* FindForResume(const std::string& token) {
// (F) 만료(ResumeWindow) 검사/정리 로직이 없다. 락도 없다.
auto it = byToken_.find(token);
return it == byToken_.end() ? nullptr : it->second;
}
void Register(GameSession* s) { byToken_[s->resumeToken] = s; }
}; 내 리뷰 · C#
내 답안 · 자동 저장
작성 후 위 해설 보기에서 모범 해설과 대조하세요.
내 리뷰 · C++
내 답안 · 자동 저장
작성 후 위 해설 보기에서 모범 해설과 대조하세요.