콘솔 GameClient를 대체하는 브라우저 픽셀 클라이언트. 실제 IOCP_Server(TCP·IOCP)에 붙어 다른 플레이어의 이동·시야(AOI)·채팅을 실시간 렌더한다. 서버는 무수정 — 브라우저는 WebSocket만 쓰고, 그 사이 얇은 WS↔TCP 릴레이가 서버의 바이너리 프로토콜을 그대로 흘려보낸다.
브라우저(live.html) ──WebSocket──▶ relay.js(Node) ──TCP──▶ IOCP_Server(무수정)
▲
MMOStressClient(더미 N)
소스: MMO/WebClient/ (live.html · proto.js · relay.js · start-demo)
1. WebSocket ↔ TCP 릴레이
브라우저는 raw TCP 소켓을 못 연다(WebSocket만 가능). 그래서 WS↔TCP 바이트 브릿지(relay.js, Node net+ws)를 두어 서버 TCP를 그대로 중계 — 서버·프로토콜 무변경. (websocat 대신 자작 → 외부 설치 0.)
2. 스트림 재조립 (length-prefix reframing)
WebSocket은 메시지(프레임) 단위지만 서버는 TCP 바이트 스트림 + 2바이트 길이 프리픽스. WS 프레임 경계 ≠ 패킷 경계가 핵심 함정. 수신 바이트를 버퍼에 누적하고 header.size로 다시 잘라 패킷 복원(proto.js의 Reassembler). TCP를 읽듯 처리.
3. 바이너리 파싱 (ArrayBuffer / DataView, little-endian, pack(1))
ws.binaryType='arraybuffer' → DataView.getUint16(off, true) / getFloat32(off, true)로 C++ #pragma pack(1) 구조체를 오프셋 그대로 읽음. proto.js = Shared/Protocol/Protocol.h 1:1 (헤더 4B = size u16 + type u16, MsgType 1000~1021).
4. UTF-16LE 채팅 디코드
서버 채팅 문자는 char16_t(Protocol.h의 ChatChar, 2바이트 고정) → JS TextDecoder('utf-16le') + 널 종단 잘라냄. 원래 wchar_t였으나 리눅스 이식 때 폭을 고정하려고 교체 — 와이어 폭은 2바이트로 같아 동작은 그대로. JS 문자열이 내부적으로 UTF-16이어도 와이어 바이트는 명시적으로 디코드해야 한글이 안 깨진다.
5. 하트비트
서버 세션 타임아웃 60초 → 클라가 15초마다 C2S_HEARTBEAT(헤더 4B) 송신으로 연결 유지.
6. 픽셀 퍼펙트 업스케일
ctx.imageSmoothingEnabled=false + CSS image-rendering:pixelated → LPC 스프라이트를 nearest-neighbor로 선명하게 확대(뭉개짐 방지).
7. DPR 대응 캔버스