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#
내 답안 · 자동 저장

작성 후 위 해설 보기에서 모범 해설과 대조하세요.