12. 게이트웨이의 존 서버 메시지 포워딩 (프레이밍/버전/멀티플렉싱) · 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) 공유 연결에 락 없는 송신 — 멀티플렉싱 인터리브 (동시성) ★간판
- 여러 워커가 동시에
RawSend→NetworkStream.Write가 한 소켓에 동시 호출. 쓰기가 인터리브되어 프레임이 섞인다. 송신 큐 + 단일 송신자로 직렬화 필요.
(E) payloadLen 언더플로/범위 검증 부재 — 보안/견고성
len - 2에서len < 2면uint언더플로로 거대값 →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, 디싱크 조기 감지 알람. 공유 연결은 한 번 깨지면 전 세션이 죽는다.
면접 포인트
- 핵심: 길이 프리픽스 정의, 엔디안 일관성, 버전 변환, 공유 연결 멀티플렉싱의 파급(한 프레임 오류 = 전 세션 붕괴).
- 예상 질문:
- "한 프레임 안에서 엔디안이 섞이면 어떻게 잡나?" → BE 헬퍼로 모든 정수를 통일,
BitConverter.GetBytes직접 사용 금지. - "왜 zlen 하나 틀렸는데 전부 죽나?" → 공유 스트림 경계 누적 밀림.
- "버전 다른 존으로 보낼 때 안전한 계약은?" → 안정 와이어 ID + 변환 어댑터, 미지원 거부.
- "한 프레임 안에서 엔디안이 섞이면 어떻게 잡나?" → BE 헬퍼로 모든 정수를 통일,
해설 — 게이트웨이의 존 서버 메시지 포워딩 (프레이밍/버전/멀티플렉싱) · C++
난이도: 최상
답변 프레임워크: 요약 → 문제 분류 → 원인 → 수정안 → 더 나은 설계
요약
이 포워더는 공유 연결 멀티플렉싱에서 치명적인 4중 결함을 갖는다. (A) 존 프레임의 길이
프리픽스 zlen 에 라우팅 헤더(sessionId 8 + zoneId 2)를 포함하지 않아 존 서버가 프레임
경계를 12바이트 짧게 잡고, 그 결과 공유 연결의 모든 후속 메시지가 디싱크된다(한 번의
오프바이로 전 세션 붕괴). (B) sessionId/zoneId 를 호스트 바이트 순서로 그대로 복사해
프로토콜의 빅엔디안 약속을 어겨, 엔디안이 다른 존 서버에서 오라우팅된다. (C) msgType 을
버전 변환 없이 전달해, 번호 체계가 다른 버전의 존에서 오디스패치된다. (D) 공유 존
연결에 락 없이 RawSend 하므로 여러 워커의 부분 쓰기가 인터리브되어 스트림이 깨진다.
부수적으로 (E) payloadLen = len - 2 에 검증이 없어 len < 2 면 언더플로로 거대한 길이가
되어 힙 오버리드. 정답의 한 줄: zlen 은 라우팅 헤더 포함 총량으로, 모든 정수는
빅엔디안으로, msgType 은 버전 변환 테이블로, 공유 연결 송신은 프레임 단위로 직렬화.
문제점
(A) 길이 프리픽스가 라우팅 헤더 미반영 — 프레이밍 디싱크 (정확성) ★간판
- 증상: 존 서버가 프레임 경계를 잘못 잡고, 공유 연결의 모든 후속 메시지가 깨진다.
- 재현 조건: 존 프레임 본문 = sessionId(8)+zoneId(2)+msgType(2)+payload = 12+payloadLen.
그러나
zlen = len = 2 + payloadLen만 기록. 존이zlen만큼 읽으면 12바이트 부족 → 다음 프레임 헤더를 본문 중간에서 읽기 시작 → 영구 디싱크. - 근본 원인: 길이 프리픽스는 "그 뒤 따라오는 전체 바이트 수" 여야 하는데 원본 프레임의
len 을 그대로 재사용했다.
zlen = 8 + 2 + 2 + payloadLen이어야 한다. - 파급: 멀티플렉싱 공유 연결에서는 한 프레임의 오프바이가 전 세션을 오염시킨다.
(B) 라우팅 헤더 엔디안 불일치 — 직렬화 (정확성/이식성)
- 증상: 존 서버가 sessionId/zoneId 를 거꾸로 읽어 엉뚱한 세션/존으로 라우팅.
- 재현 조건: 프로토콜은 빅엔디안 약속인데
memcpy(&sessionId,...)는 호스트(리틀엔디안 x86) 순서로 기록. 존 서버가 빅엔디안으로 읽으면 바이트가 뒤집힌다. (참고: 64비트는ntohll이 POSIX 표준이 아니므로 바이트 단위 BE 헬퍼PutU64BE를 직접 둔다.) - 근본 원인: 라우팅 헤더만 변환을 빠뜨렸다. msgType 은
htons했지만 sessionId/zoneId 는 안 했다(일관성 붕괴).
(C) msgType 버전 변환 누락 — 버전/호환 (정확성) ★간판
- 증상: 롤링 배포로 버전이 다른 존에서 메시지가 잘못된 핸들러로 디스패치된다.
- 재현 조건: 게이트웨이는 클라 프로토콜 vX 의 msgType 번호를, 존은 vY 의 번호 체계를 기대. 번호가 재배치/추가/삭제됐다면 같은 숫자가 다른 의미 → 오디스패치(또는 미정의 타입으로 연결 종료).
- 근본 원인: 게이트웨이가 버전 경계의 변환 책임을 지는데, msgType(및 페이로드 스키마) 변환 테이블 없이 그대로 흘려보냈다.
(D) 공유 연결에 락 없는 송신 — 멀티플렉싱 인터리브 (동시성) ★간판
- 증상: 부하 시 존 스트림이 깨진다(서로 다른 세션 프레임이 섞임).
- 재현 조건: 워커 T1/T2 가 동시에
ForwardToZone→RawSend가 한 소켓에 동시 write. TCP write 는 원자적이지 않아(특히 부분 쓰기) 두 프레임의 바이트가 인터리브 → 디싱크. - 근본 원인: 한 소켓에 동시에 둘 이상 write 하면 안 된다. 프레임 단위로 송신 직렬화 (송신 큐 + 단일 송신자)가 필요(session_network/problem5 와 연결되나, 여기선 멀티플렉싱 프레이밍 정합성이 핵심).
(E) payloadLen 언더플로/검증 부재 — 보안/견고성
payloadLen = len - 2에서len < 2(손상/악성 프레임)면uint32_t언더플로로 ~4G →payload힙 오버리드/거대 할당.frameLen == 4 + len,len >= 2, 상한 검증이 필요.
수정안
핵심: ① zlen 은 라우팅 헤더 포함 총량, ② 모든 정수 빅엔디안 일관, ③ msgType 버전 변환, ④ 공유 연결 프레임 단위 직렬화, ⑤ 길이 검증.
void ForwardToZone(uint64_t sessionId, uint16_t zoneId,
const uint8_t* clientFrame, size_t frameLen)
{
if (frameLen < 6) { Drop(sessionId); return; } // (E) 최소 길이
uint32_t len; std::memcpy(&len, clientFrame, 4); len = ntohl(len);
if (len < 2 || frameLen != 4u + len) { Drop(sessionId); return; } // (E) 정합성
uint16_t msgType; std::memcpy(&msgType, clientFrame + 4, 2); msgType = ntohs(msgType);
const uint8_t* payload = clientFrame + 6;
const uint32_t payloadLen = len - 2;
// (C) 버전 변환: 클라 msgType → 존 버전 msgType (+ 필요시 페이로드 스키마 변환)
uint16_t zoneMsgType;
if (!TranslateMsgType(zone_->version, msgType, zoneMsgType)) { Drop(sessionId); return; }
const uint32_t body = 8 + 2 + 2 + payloadLen; // (A) 본문 총량
std::vector<uint8_t> out(4 + body);
size_t off = 0;
PutU32BE(out, off, body); off += 4; // (A) zlen = body
PutU64BE(out, off, sessionId); off += 8; // (B) 빅엔디안
PutU16BE(out, off, zoneId); off += 2; // (B)
PutU16BE(out, off, zoneMsgType); off += 2; // (C)
std::memcpy(out.data() + off, payload, payloadLen);
zone_->SendFrame(out); // (D) 내부에서 송신 큐로 프레임 단위 직렬화
}
SendFrame은 존 연결마다 송신 큐 + 단일 송신 스레드(또는 송신 락)로 한 프레임을 쪼개지 않고 순서대로 write. 부분 쓰기도 큐가 흡수.PutU64BE/PutU32BE/PutU16BE로 모든 정수를 빅엔디안으로 통일.
더 나은 설계
1) 버전 경계의 명시적 어댑터
- 게이트웨이가 "버전 변환 계층" 을 1급으로 둔다: msgType 매핑 + 페이로드 스키마 up/down 컨버터. 미지원 조합은 명확히 거부. 트레이드오프: 변환 비용/유지보수 ↔ 무중단 배포 가능.
2) 멀티플렉싱 프로토콜 정식화
- 직접 헤더를 붙이기보다 streamId/length 가 표준화된 멀티플렉싱(HTTP/2·QUIC·gRPC 류)을 쓰면 프레이밍·flow control·HOL 완화가 검증된 형태로 제공. 자체 구현 시엔 프레임 경계와 송신 직렬화를 절대 깨지 말 것.
3) 길이/한계 방어
- 최소·최대 프레임 크기, len↔frameLen 정합, payloadLen 상한을 모든 경계에서 검증. 손상/악성 프레임이 공유 연결을 오염시키지 못하게 한다.
4) 연결당 단일 송신자
- 존 연결마다 송신 큐 + 단일 소비자. 여러 워커는 enqueue 만. 백프레셔는 큐 한계/드롭 정책으로(우선순위 큐로 중요한 프레임 보호).
5) 관측성
- 프레임 카운터/CRC, 디싱크 감지(예상 길이 vs 실제) 알람. 멀티플렉싱은 한 번 깨지면 전 세션이 죽으므로 조기 감지가 중요.
면접 포인트
- 핵심: 길이 프리픽스의 정의("그 뒤 전체 바이트"), 엔디안 일관성, 버전 경계 변환, 그리고 공유 연결 멀티플렉싱에서 한 프레임의 오류가 전 세션을 죽인다는 파급 인식.
- 예상 질문:
- "zlen 을 잘못 계산하면 왜 한 세션이 아니라 전부 죽나?" → 공유 스트림에서 경계가 한 번 밀리면 이후 모든 프레임 헤더가 어긋난다(누적 디싱크).
- "한 소켓에 두 스레드가 write 하면?" → 부분 쓰기 인터리브로 프레임 혼합. 송신 큐로 프레임 단위 직렬화.
- "롤링 배포로 게이트웨이/존 버전이 다를 때 msgType 을 어떻게 다루나?" → 변환 테이블/ 어댑터, 미지원은 거부, 안정 와이어 ID 계약.
구문 검증:
g++ -std=c++17 -fsyntax-only problem.cpp통과.