8. 대용량 메시지 프래그먼트/재조립

난이도 상 해설 보기 →

결함을 모두 찾고 원인·수정안·더 나은 설계를 제시하라. 마커 (A)(B) 는 주목 위치 힌트다.

결함 코드 · C#
// ============================================================================
// [코드리뷰 문제] C# - 대용량 메시지 프래그먼트/재조립 (UDP 신뢰 계층)
// ----------------------------------------------------------------------------
// 시나리오:
//   - 게임은 자체 신뢰성 UDP 계층 위에서 동작한다. MTU(예: 1200B)를 넘는
//     큰 메시지(맵 스트리밍, 인벤토리 풀스냅샷)는 여러 프래그먼트로 쪼개 보낸다.
//   - 프래그먼트 헤더(와이어, little-endian):
//       [uint msgId][ushort fragIndex][ushort fragCount][payload...]
//     같은 msgId 의 프래그먼트들을 fragIndex 순서로 재조립해 완성 메시지를 만든다.
//   - 신뢰 계층이 순서/중복은 어느 정도 처리하지만, 재전송/지연으로
//     프래그먼트가 중복·지연·유실되어 도착할 수 있다(타이밍은 비결정적).
//   - 클라이언트는 신뢰할 수 없다(변조된 프래그먼트 헤더 가능).
//
// 요구사항:
//   - 변조/거대 메시지/유실 상황에서도 서버 메모리가 무한히 늘거나 깨지면 안 된다.
//   - 동시에 여러 메시지가 재조립 중일 수 있다.
//
// 과제:
//   이 코드의 잠재적 문제를 모두 찾고, 왜/어떻게 악용·오작동하는지 설명하고,
//   수정안과 더 나은 재조립 설계를 제시하라.
// ============================================================================

using System;
using System.Collections.Generic;

// 재조립 진행 중인 메시지 상태
public class ReassemblyState
{
    public ushort fragCount = 0;
    public ushort received  = 0;
    public byte[] buffer;                 // 완성 메시지를 여기에 채운다
    public bool[] got;                    // 각 프래그먼트 도착 여부
}

public class Reassembler
{
    private const int HeaderSize = 8;     // uint + ushort + ushort

    private Dictionary<uint, ReassemblyState> _pending = new Dictionary<uint, ReassemblyState>();

    // 한 프래그먼트 수신
    public void OnFragment(byte[] buf, int len)
    {
        uint   msgId     = BitConverter.ToUInt32(buf, 0);
        ushort fragIndex = BitConverter.ToUInt16(buf, 4);
        ushort fragCount = BitConverter.ToUInt16(buf, 6);

        int payloadOffset = HeaderSize;
        int payloadLen    = len - HeaderSize;          // (A)

        // (B) 이 msgId 의 재조립 상태를 가져오거나 새로 만든다
        if (!_pending.TryGetValue(msgId, out ReassemblyState st))
        {
            st = new ReassemblyState();
            _pending[msgId] = st;
        }

        if (st.fragCount == 0)
        {
            // 첫 프래그먼트: 전체 크기를 fragCount 로 예약
            st.fragCount = fragCount;
            st.got = new bool[fragCount];
            // (C) 한 프래그먼트당 payloadLen 으로 전체 버퍼 크기 추정
            st.buffer = new byte[fragCount * payloadLen];
        }

        // (D) fragIndex 위치에 payload 복사
        int offset = fragIndex * payloadLen;
        Array.Copy(buf, payloadOffset, st.buffer, offset, payloadLen);

        if (!st.got[fragIndex])
        {
            st.got[fragIndex] = true;
            st.received++;
        }

        // (E) 다 모였으면 완성 메시지 디스패치
        if (st.received == st.fragCount)
        {
            OnMessageComplete(msgId, st.buffer, st.buffer.Length);
            _pending.Remove(msgId);
        }
    }

    private void OnMessageComplete(uint msgId, byte[] data, int len) { /* ... */ }
}
내 리뷰 · C#
내 답안 · 자동 저장

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