← 문제로

12. 게이트웨이의 존 서버 메시지 포워딩 (프레이밍/버전/멀티플렉싱) · C#

난이도 최상
내 리뷰 · C#
해설 · C#

해설 — 게이트웨이의 존 서버 메시지 포워딩 (프레이밍/버전/멀티플렉싱) · C#

난이도: 최상

답변 프레임워크: 요약 → 문제 분류 → 원인 → 수정안 → 더 나은 설계

요약

공유 연결 멀티플렉싱 포워더의 4중 결함이다. (A) 존 프레임 길이 프리픽스 zlen라우팅 헤더(sessionId 8 + zoneId 2)를 포함하지 않아 존이 경계를 12바이트 짧게 잡고 공유 연결 전체가 디싱크된다. (B) sessionId/zoneId 를 BitConverter.GetBytes호스트(리틀엔디안) 바이트 순서로 써, 빅엔디안 약속을 어기고 오라우팅된다(반면 msgType 은 BE 로 써서 한 프레임 안에서도 엔디안이 섞임). (C) msgType 을 버전 변환 없이 전달해 버전 다른 존에서 오디스패치. (D) 공유 ZoneLink락 없이 RawSend 하여 여러 스레드의 쓰기가 인터리브. 부수적으로 (E) len - 2 언더플로/Array.Copy 범위 검증 부재. 정답의 한 줄: zlen=라우팅 헤더 포함 총량, 정수는 모두 빅엔디안, msgType 버전 변환, 공유 연결 프레임 단위 직렬화, 길이 검증.


문제점

(A) 길이 프리픽스가 라우팅 헤더 미반영 — 프레이밍 디싱크 (정확성) ★간판

  • 증상: 존이 경계를 잘못 잡아 공유 연결의 모든 후속 메시지가 깨진다.
  • 재현 조건: 본문 = sessionId(8)+zoneId(2)+msgType(2)+payload = 12+payloadLen 인데 zlen = len = 2 + payloadLen. 존이 zlen 만 읽으면 12바이트 부족 → 영구 디싱크.
  • 근본 원인: 길이 프리픽스는 "뒤따르는 전체 바이트 수". zlen = 8+2+2+payloadLen 이어야.

(B) 라우팅 헤더 엔디안 불일치 — 직렬화 (정확성/이식성)

  • 증상: 존이 sessionId/zoneId 를 뒤집어 읽어 오라우팅.
  • 재현 조건: BitConverter.GetBytes(sessionId) 는 호스트 순서(x86 리틀엔디안). msgType 은 WriteU16BE 로 빅엔디안 → 한 프레임 안에서 엔디안이 섞인다. 존이 BE 로 읽으면 라우팅 헤더만 깨진다.
  • 근본 원인: 라우팅 헤더 변환 누락. IPAddress.HostToNetworkOrder 또는 BE 쓰기로 통일.

(C) msgType 버전 변환 누락 — 버전/호환 (정확성) ★간판

  • 게이트웨이(클라 vX)와 존(vY)의 msgType 번호 체계가 다르면 같은 숫자가 다른 핸들러로 디스패치. 변환 테이블/어댑터가 필요.

(D) 공유 연결에 락 없는 송신 — 멀티플렉싱 인터리브 (동시성) ★간판

  • 여러 워커가 동시에 RawSendNetworkStream.Write 가 한 소켓에 동시 호출. 쓰기가 인터리브되어 프레임이 섞인다. 송신 큐 + 단일 송신자로 직렬화 필요.

(E) payloadLen 언더플로/범위 검증 부재 — 보안/견고성

  • len - 2 에서 len < 2uint 언더플로로 거대값 → new byte[...]/Array.Copy 에서 OverflowException/ArgumentException(또는 거대 할당). frameLen == 4 + len, len >= 2, 상한 검증 필요.

수정안

public void ForwardToZone(ulong sessionId, ushort zoneId, byte[] clientFrame, int frameLen)
{
    if (frameLen < 6) { Drop(sessionId); return; }                     // (E)
    uint len = (uint)IPAddress.NetworkToHostOrder(BitConverter.ToInt32(clientFrame, 0));
    if (len < 2 || frameLen != 4 + (int)len) { Drop(sessionId); return; } // (E)
    ushort msgType = (ushort)IPAddress.NetworkToHostOrder(BitConverter.ToInt16(clientFrame, 4));
    int payloadOffset = 6;
    int payloadLen = (int)len - 2;

    if (!TranslateMsgType(_zone.Version, msgType, out ushort zoneMsgType))  // (C)
    { Drop(sessionId); return; }

    int body = 8 + 2 + 2 + payloadLen;                                  // (A)
    byte[] outBuf = new byte[4 + body];
    int off = 0;
    WriteU32BE(outBuf, off, (uint)body); off += 4;                     // (A) zlen=body
    WriteU64BE(outBuf, off, sessionId);  off += 8;                     // (B) BE 통일
    WriteU16BE(outBuf, off, zoneId);     off += 2;                     // (B)
    WriteU16BE(outBuf, off, zoneMsgType);off += 2;                     // (C)
    Array.Copy(clientFrame, payloadOffset, outBuf, off, payloadLen);

    _zone.SendFrame(outBuf);   // (D) 송신 큐로 프레임 단위 직렬화
}
  • WriteU64BE 를 추가해 sessionId 도 빅엔디안으로. SendFrame 은 연결별 큐 + 단일 송신 태스크로 프레임을 쪼개지 않고 순서대로 보낸다.

더 나은 설계

1) 버전 경계 어댑터

  • msgType 매핑 + 페이로드 스키마 up/down 컨버터를 1급 계층으로. 미지원 조합은 거부. 무중단 롤링 배포를 가능케 함(트레이드오프: 변환 비용/유지보수).

2) 검증된 멀티플렉싱

  • 자체 헤더 대신 HTTP/2·gRPC·QUIC 류 표준 멀티플렉싱을 쓰면 프레이밍·flow control·HOL 완화가 검증된 형태로 제공.

3) 길이/한계 방어

  • 최소·최대 프레임, len↔frameLen 정합, payloadLen 상한을 모든 경계에서.

4) 연결당 단일 송신자 + 백프레셔

  • 존 연결마다 송신 큐 + 단일 소비자. 워커는 enqueue 만. 큐 한계/우선순위로 백프레셔.

5) 관측성

  • 프레임 카운터/CRC, 디싱크 조기 감지 알람. 공유 연결은 한 번 깨지면 전 세션이 죽는다.

면접 포인트

  • 핵심: 길이 프리픽스 정의, 엔디안 일관성, 버전 변환, 공유 연결 멀티플렉싱의 파급(한 프레임 오류 = 전 세션 붕괴).
  • 예상 질문:
    1. "한 프레임 안에서 엔디안이 섞이면 어떻게 잡나?" → BE 헬퍼로 모든 정수를 통일, BitConverter.GetBytes 직접 사용 금지.
    2. "왜 zlen 하나 틀렸는데 전부 죽나?" → 공유 스트림 경계 누적 밀림.
    3. "버전 다른 존으로 보낼 때 안전한 계약은?" → 안정 와이어 ID + 변환 어댑터, 미지원 거부.