42. 기능 플래그(capability) 협상: 교집합 부재와 협상 결과의 잘못된 캐시 범위 (C#)

난이도 중 해설 보기 →

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

결함 코드 · C#
// [시나리오]
// 게임 클라이언트와 서버는 접속 시 핸드셰이크로 "기능 플래그(capability bits)"를 교환한다.
// 클라가 자신이 지원하는 기능 비트를 보내면, 서버는 이후 그 세션에 대해 어떤 인코딩·기능을
// 쓸지 결정한다. 기능이 늘어나면서 비트가 추가되고, 구버전 클라도 함께 접속한다.
//
// 기능 비트:
//   0x01 DeltaSnapshot  - 델타 스냅샷 전송
//   0x02 Lz4Compress    - LZ4 압축 페이로드
//   0x04 BatchedEvents  - 이벤트 배치 전송
//   0x08 VoiceRelay     - 음성 릴레이 (서버 v3.2 부터 지원)
//   0x10 Reserved       - 미사용(향후 확장)
//
// [요구사항]
// - 서버는 자신이 실제로 지원하는 기능만 활성화해야 한다.
// - 협상 결과는 그 연결(세션)에만 적용된다. 재접속하면 다시 협상한다.
// - 클라가 보낸 값은 신뢰할 수 없는 입력이다.
//
// [과제]
// (A), (B) 지점을 중심으로 이 협상 로직이 요구사항을 만족하지 못하는 조건을 찾고,
// 재현 시나리오와 수정안을 제시하시오.

using System;
using System.Collections.Concurrent;

namespace GameServer.Protocol
{
    [Flags]
    public enum Capabilities : uint
    {
        None = 0,
        DeltaSnapshot = 0x01,
        Lz4Compress = 0x02,
        BatchedEvents = 0x04,
        VoiceRelay = 0x08,
    }

    public sealed class Session
    {
        public long AccountId;
        public Capabilities Caps;
        public ushort ClientProtocolVersion;
    }

    public sealed class CapabilityNegotiator
    {
        // 서버가 이번 빌드에서 실제로 지원하는 기능
        public const Capabilities ServerSupported =
            Capabilities.DeltaSnapshot | Capabilities.Lz4Compress | Capabilities.BatchedEvents;

        private static readonly ConcurrentDictionary<long, Capabilities> _cache = new();

        public void OnHandshake(Session session, ReadOnlySpan<byte> payload)
        {
            ushort version = BitConverter.ToUInt16(payload.Slice(0, 2));
            uint clientCaps = BitConverter.ToUInt32(payload.Slice(2, 4));

            session.ClientProtocolVersion = version;

            // (A)
            if (_cache.TryGetValue(session.AccountId, out var cached))
            {
                session.Caps = cached;
                SendHandshakeAck(session, cached);
                return;
            }

            // (B)
            session.Caps = (Capabilities)clientCaps;
            _cache[session.AccountId] = session.Caps;

            SendHandshakeAck(session, session.Caps);
        }

        public byte[] Encode(Session session, byte[] body)
        {
            if (session.Caps.HasFlag(Capabilities.Lz4Compress))
                body = Lz4.Compress(body);
            if (session.Caps.HasFlag(Capabilities.VoiceRelay))
                body = VoiceRelay.Wrap(body);
            return body;
        }

        private void SendHandshakeAck(Session s, Capabilities caps) { /* ... */ }
    }

    internal static class Lz4 { public static byte[] Compress(byte[] b) => b; }
    internal static class VoiceRelay { public static byte[] Wrap(byte[] b) => b; }
}
내 리뷰 · C#
내 답안 · 자동 저장

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