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++
// [시나리오]
// 게임 클라이언트와 C++ 서버가 접속 시 핸드셰이크로 "기능 플래그(capability bits)"를 교환한다.
// 클라가 지원 기능 비트를 보내면 서버는 그 세션에서 쓸 인코딩·기능을 결정한다.
//
// 기능 비트:
// 0x01 DeltaSnapshot - 델타 스냅샷 전송
// 0x02 Lz4Compress - LZ4 압축 페이로드
// 0x04 BatchedEvents - 이벤트 배치 전송
// 0x08 VoiceRelay - 음성 릴레이 (서버 v3.2 부터 지원)
// 0x10 Reserved - 미사용(향후 확장)
//
// [요구사항]
// - 서버는 자신이 실제로 지원하는 기능만 활성화한다.
// - 협상 결과는 해당 연결에만 적용되고, 재접속 시 다시 협상한다.
// - 클라가 보낸 바이트열은 신뢰할 수 없는 입력이다.
//
// [과제]
// (A), (B) 지점을 중심으로 요구사항이 깨지는 조건을 찾고, 재현 시나리오와 수정안을 제시하시오.
#include <cstdint>
#include <cstring>
#include <mutex>
#include <string>
#include <unordered_map>
#include <vector>
namespace gs {
enum Capability : std::uint32_t {
CAP_NONE = 0,
CAP_DELTA_SNAPSHOT = 0x01,
CAP_LZ4 = 0x02,
CAP_BATCHED_EVENTS = 0x04,
CAP_VOICE_RELAY = 0x08,
};
constexpr std::uint32_t kServerSupported =
CAP_DELTA_SNAPSHOT | CAP_LZ4 | CAP_BATCHED_EVENTS;
struct Session {
std::int64_t accountId = 0;
std::uint32_t caps = CAP_NONE;
std::uint16_t clientProtocolVersion = 0;
};
std::vector<std::uint8_t> lz4Compress(const std::vector<std::uint8_t>& b) { return b; }
std::vector<std::uint8_t> voiceRelayWrap(const std::vector<std::uint8_t>& b) { return b; }
class CapabilityNegotiator {
public:
void onHandshake(Session& session, const std::uint8_t* payload, std::size_t len) {
std::uint16_t version = 0;
std::uint32_t clientCaps = 0;
std::memcpy(&version, payload, sizeof(version));
std::memcpy(&clientCaps, payload + 2, sizeof(clientCaps));
session.clientProtocolVersion = version;
// (A)
{
std::lock_guard<std::mutex> lk(mutex_);
auto it = cache_.find(session.accountId);
if (it != cache_.end()) {
session.caps = it->second;
sendHandshakeAck(session, session.caps);
return;
}
}
// (B)
session.caps = clientCaps;
{
std::lock_guard<std::mutex> lk(mutex_);
cache_[session.accountId] = session.caps;
}
sendHandshakeAck(session, session.caps);
}
std::vector<std::uint8_t> encode(const Session& session,
std::vector<std::uint8_t> body) {
if (session.caps & CAP_LZ4) body = lz4Compress(body);
if (session.caps & CAP_VOICE_RELAY) body = voiceRelayWrap(body);
return body;
}
private:
void sendHandshakeAck(const Session&, std::uint32_t) {}
std::mutex mutex_;
std::unordered_map<std::int64_t, std::uint32_t> cache_;
};
} // namespace gs
int main() { return 0; } 내 리뷰 · C#
내 답안 · 자동 저장
작성 후 위 해설 보기에서 모범 해설과 대조하세요.
내 리뷰 · C++
내 답안 · 자동 저장
작성 후 위 해설 보기에서 모범 해설과 대조하세요.