← posts/b.log()

blog92@web:~$ cat posts/websocket-07-control-frames-ping-pong-close.md

NETWORK2 min read

WebSocket 완전 정복 7편 — 제어 프레임: Ping/Pong 하트비트와 Close 코드

half-open 연결과 유휴 타임아웃 때문에 하트비트가 필요한 이유, ws로 구현하는 서버 주도 Ping/Pong과 주기를 정하는 기준, 그리고 1000부터 4999까지 Close 코드 표와 실무에서 가장 흔한 1006의 원인을 다룹니다.

WebSocket 완전 정복 · 2부. 중급: 프로토콜 내부와 설계 패턴 · 16편 중 7편

이전 편: 6편 — 프레임 구조

6편에서 opcode 0x8·0x9·0xA가 제어 프레임이라는 것까지 봤습니다. 이번 편은 그 셋을 실제로 씁니다 — 죽은 연결을 찾아내는 Ping/Pong 하트비트를 서버가 주도해야 하는 이유와 주기를 정하는 기준, 그리고 끊길 때 이유를 전하는 Close 코드입니다.

Ping/Pong — 하트비트는 서버가 주도한다

Ping을 받으면 같은 페이로드로 Pong을 응답해야 합니다. 브라우저는 서버 Ping에 자동 응답하지만 JS에서 Ping을 보내는 API는 없으므로 하트비트는 서버가 주도합니다.

하트비트가 필요한 이유:

  • TCP는 상대가 사라져도 한참 모를 수 있음(half-open 연결).
  • NAT·프록시·LB가 유휴 연결을 조용히 끊음.
ts
import { WebSocketServer, WebSocket } from "ws";
 
type LiveSocket = WebSocket & { isAlive?: boolean };
const wss = new WebSocketServer({ port: 8080 });
 
wss.on("connection", (ws: LiveSocket) => {
  ws.isAlive = true;
  ws.on("pong", () => { ws.isAlive = true; });
});
 
const interval = setInterval(() => {
  for (const ws of wss.clients as Set<LiveSocket>) {
    if (!ws.isAlive) { ws.terminate(); continue; } // 응답 없음 → 강제 종료
    ws.isAlive = false;
    ws.ping();
  }
}, 30_000);
 
wss.on("close", () => clearInterval(interval));

주기는 경로상 가장 짧은 유휴 타임아웃보다 짧게. Nginx proxy_read_timeout 기본 60초, 많은 클라우드 LB 기본값도 60초 전후 → 20~30초가 흔함.

Close 코드

코드의미
1000정상 종료
1001떠남 (페이지 이동, 서버 재시작)
1002프로토콜 오류
1003지원하지 않는 데이터
1005상태 코드 없음 (예약, 전송 금지)
1006비정상 종료. Close 프레임 없이 끊김 (예약, 전송 금지)
1007잘못된 페이로드 (예: 깨진 UTF-8)
1008정책 위반 (인증 실패 등)
1009메시지가 너무 큼
1011서버 내부 오류
4000~4999애플리케이션 정의

1006은 실무에서 가장 흔합니다. 원인은 네트워크 단절, 프록시 타임아웃, 서버 크래시 등 다양하므로 인프라 타임아웃부터 의심하세요. 재연결하면 안 되는 상황(인증 만료 등)은 4000번대 코드로 알려주면 클라이언트 로직이 깔끔해집니다.

더 깊이

COMMENTS (…)

댓글을 불러오는 중이에요.

NEW COMMENT0 / 1000
⌘↵ 전송

blog92@web:~$ cd ..