ws 서버: 브로드캐스트와 끊긴 연결 감지(하트비트)
ws 서버: 브로드캐스트와 끊긴 연결 감지(하트비트)
ws로 서버를 만들다 보면 연결된 모든 클라이언트에게 같은 메시지를 뿌리는 브로드캐스트를 자주 구현하게 돼요. 또 클라이언트가 갑자기 끊겨도 서버는 모를 수 있는데, 이때 ping/pong을 이용한 하트비트로 끊긴 연결을 찾아내야 하죠. 이 글에서는 ws 서버에서 브로드캐스트를 하는 방법과 하트비트로 연결 수명주기를 관리하는 패턴을 살펴볼게요.
본문
서버 브로드캐스트
연결된 모든 WebSocket 클라이언트(자신 포함)에게 메시지를 보내려면 wss.clients를 돌면서 OPEN 상태인 클라이언트에게만 전달하면 돼요. 받은 데이터가 텍스트인지 바이너리인지에 따라 { binary: isBinary } 옵션을 함께 넘기는 게 포인트예요.
import WebSocket, { WebSocketServer } from 'ws';
const wss = new WebSocketServer({ port: 8080 });
wss.on('connection', function connection(ws) {
ws.on('error', console.error);
ws.on('message', function message(data, isBinary) {
wss.clients.forEach(function each(client) {
if (client.readyState === WebSocket.OPEN) {
client.send(data, { binary: isBinary });
}
});
});
});
자기 자신을 제외한 다른 클라이언트에게만 보내고 싶으면 client !== ws 조건을 추가해요.
import WebSocket, { WebSocketServer } from 'ws';
const wss = new WebSocketServer({ port: 8080 });
wss.on('connection', function connection(ws) {
ws.on('error', console.error);
ws.on('message', function message(data, isBinary) {
wss.clients.forEach(function each(client) {
if (client !== ws && client.readyState === WebSocket.OPEN) {
client.send(data, { binary: isBinary });
}
});
});
});
끊긴 연결 감지와 하트비트
서버와 클라이언트 사이의 링크가 (예를 들어 케이블을 뽑는 것처럼) 끊겨도 양쪽이 그 상태를 모를 수 있어요. 이때 ping 메시지를 써서 상대가 여전히 응답하는지 확인할 수 있습니다. ws는 명세에 따라 ping에 대한 pong을 자동으로 보내 줘요.
서버는 주기적으로(setInterval) 각 클라이언트에 ping()을 보내고, pong 이벤트(heartbeat)가 오면 isAlive를 다시 true로 표시해요. 다음 주기에도 isAlive === false인 클라이언트는 연결이 끊어진 것으로 보고 ws.terminate()로 정리합니다.
import { WebSocketServer } from 'ws';
function heartbeat() {
this.isAlive = true;
}
const wss = new WebSocketServer({ port: 8080 });
wss.on('connection', function connection(ws) {
ws.isAlive = true;
ws.on('error', console.error);
ws.on('pong', heartbeat);
});
const interval = setInterval(function ping() {
wss.clients.forEach(function each(ws) {
if (ws.isAlive === false) return ws.terminate();
ws.isAlive = false;
ws.ping();
});
}, 30000);
wss.on('close', function close() {
clearInterval(interval);
});
클라이언트 쪽도 연결이 끊긴 걸 모를 수 있으니 ping 리스너를 달아 주는 게 좋아요. WebSocket#close()는 닫기 타이머를 기다리지만, WebSocket#terminate()는 연결을 즉시 파괴한다는 차이가 있어요. 여기서는 지연 시간을 보수적으로 잡아 ping 주기(30초)에 1초를 더한 30000 + 1000을 타임아웃으로 씁니다.
import WebSocket from 'ws';
function heartbeat() {
clearTimeout(this.pingTimeout);
// Use `WebSocket#terminate()`, which immediately destroys the connection,
// instead of `WebSocket#close()`, which waits for the close timer.
// Delay should be equal to the interval at which your server
// sends out pings plus a conservative assumption of the latency.
this.pingTimeout = setTimeout(() => {
this.terminate();
}, 30000 + 1000);
}
const client = new WebSocket('wss://websocket-echo.com/');
client.on('error', console.error);
client.on('open', heartbeat);
client.on('ping', heartbeat);
client.on('close', function clear() {
clearTimeout(this.pingTimeout);
});
외부 HTTP/S 서버와 함께 쓰기
자체 HTTP 서버 없이 기존 HTTPS 서버에 WebSocket을 얹으려면 서버의 인증서(cert, key)를 읽어 createServer로 만든 뒤, 그 server를 WebSocketServer에 넘겨주면 돼요.
import { createServer } from 'https';
import { readFileSync } from 'fs';
import { WebSocketServer } from 'ws';
const server = createServer({
cert: readFileSync('/path/to/cert.pem'),
key: readFileSync('/path/to/key.pem')
});
const wss = new WebSocketServer({ server });
wss.on('connection', function connection(ws) {
ws.on('error', console.error);
ws.on('message', function message(data) {
console.log('received: %s', data);
});
ws.send('something');
});
server.listen(8080);
더 알아보기
- npm — ws 패키지
- GitHub — websockets/ws 저장소
- WebSocket 프로토콜 — Wikipedia