- Node.js
Node.js WebSocket実装|ws・再接続・認証・nginxまで
WebSocket とは、ブラウザとサーバーが1本の接続を保ったまま、どちらからでも好きなタイミングでメッセージを送れる通信方式です。 Node.js では、サーバー側に ws ライブラリ、クライアント側に Node.js 22 から標準で使える WebSocket(ブラウザと同じ API)を使うのが、依存が少なくわかりやすい構成です。
この記事では、ルーム付きチャットのサーバーとクライアントを実際に動かし、実行ログをそのまま掲載します。入門記事で省かれがちな ハートビート(切断検知)、自動再接続(指数バックオフ+ジッター)、接続時の認証、バックプレッシャー、nginx のリバースプロキシ設定 まで、すべて実行して確認しました。
検証環境: Node.js 22.22.2、ws 8.22.0(2026-09-26 公開、npm で確認)、Playwright 1.56.1(Chromium 141)、nginx 1.24.0、Linux x64。
WebSocket 実装の早見表
| やりたいこと | 書き方(ws / 標準 WebSocket) | この記事の節 |
|---|---|---|
| サーバーを立てる | new WebSocketServer({ port }) または { noServer: true } |
完成コード |
| Node.js からつなぐ | new WebSocket(url)(Node 22 は import 不要) |
クライアント |
| 全員に送る | for (const c of wss.clients) c.send(...) |
ブロードキャスト |
| ルームだけに送る | Map<ルーム名, Set<ws>> を自前で管理 |
ルーム |
| 接続時に認証 | server.on('upgrade') でトークン検証 → handleUpgrade |
認証 |
| 切れた接続を見つける | 30秒ごとに ws.ping()、pong が来なければ terminate() |
ハートビート |
| 自動再接続 | close で待ち時間 = ランダム(0〜min(上限, 基準×2^n)) |
再接続 |
| 送りすぎを防ぐ | ws.bufferedAmount が閾値を超えたら送らない |
バックプレッシャー |
| 大きすぎる受信を拒否 | maxPayload: 64 * 1024 |
完成コード |
| nginx 経由で使う | proxy_http_version 1.1 + Upgrade / Connection ヘッダー(本番は wss://) |
nginx |
| 通常の切断 | ws.close(1000, 'bye') |
終了コード |
Node.js 22 の標準 WebSocket はクライアント専用です。サーバーは ws などのライブラリで作ります。Node.js のドキュメント(globals)では、v22.0.0 でフラグ不要になり、v22.4.0 で「No longer experimental(実験的機能ではなくなった)」 と記載されています。
本記事の内容は、ご自由にお使いください。
ご利用の際は、出典として本ページへのリンクを記載いただけますようお願いします。
(記載例)出典:株式会社RJC「Node.js WebSocket実装|ws・再接続・認証・nginxまで」
ws・Socket.IO・SSE・ポーリングの使い分け
| 方式 | 通信の向き | 自動再接続 | ルーム | ブラウザ側 | 向いている用途 |
|---|---|---|---|---|---|
| ws(素の WebSocket) | 双方向 | 自作 | 自作 | 標準 WebSocket |
チャット、共同編集、ゲーム、IoT。プロトコルを自分で決めたい |
| Socket.IO(4.8.x) | 双方向 | あり | あり | 専用クライアントが必要 | 機能をまとめて使いたい。素の WebSocket クライアントとは互換なし |
| SSE(Server-Sent Events) | サーバー → クライアント | ブラウザが自動 | ― | 標準 EventSource |
通知、進捗、AI の逐次出力。HTTP のままなので構成がシンプル |
| ポーリング | クライアントが定期取得 | ― | ― | fetch |
数十秒単位の更新で十分なダッシュボード |
判断の目安: サーバーから流すだけなら SSE、再接続やルームを書く量を減らすなら Socket.IO、標準 API で軽く作る・他言語とつなぐなら ws です。
メッセージは JSON の「type」で設計する
WebSocket はただの文字列(またはバイナリ)を送る仕組みなので、メッセージの形を最初に決めておくと後が楽です。この記事では次の形にしました。
| 方向 | type | 例 | 説明 |
|---|---|---|---|
| C → S | join |
{"type":"join","room":"dev","id":1} |
ルームに入る。id は応答との対応付け用 |
| C → S | chat |
{"type":"chat","text":"こんにちは"} |
今いるルームに送る |
| C → S | announce |
{"type":"announce","text":"お知らせ"} |
全員に送る |
| S → C | welcome / joined / system |
{"type":"joined","room":"dev","id":1} |
接続・入室の通知 |
| S → C | chat |
{"type":"chat","room":"dev","from":"alice","text":"...","ts":1790674741036} |
送信者と時刻はサーバーが付ける |
| S → C | error |
{"type":"error","code":"UNKNOWN_TYPE","message":"..."} |
エラーは code で機械的に判定できるように |
ポイントは「送信者名・時刻・ルームはサーバーが付ける」ことです。クライアントが送ってきた from を信じないことで、なりすましを防げます。
完成コード:サーバー(ws 8.22)
mkdir ws-chat && cd ws-chat
npm init -y && npm pkg set type=module
npm install ws@8
mkdir public
server.mjs(HTTP と WebSocket を同じポートで提供し、upgrade 時に認証します):
// server.mjs — ws 8.x + Node.js 22
import { createServer } from 'node:http';
import { readFile } from 'node:fs/promises';
import { createHmac, timingSafeEqual } from 'node:crypto';
import { WebSocketServer, WebSocket } from 'ws';
const PORT = Number(process.env.PORT ?? 7371);
const SECRET = process.env.WS_SECRET ?? 'dev-secret-change-me';
const HEARTBEAT_MS = Number(process.env.HEARTBEAT_MS ?? 30000);
const ALLOWED_ORIGINS = new Set([`http://localhost:${PORT}`, 'http://localhost:7372']);
const MAX_BUFFERED = 1024 * 1024; // 1MB を超えて溜まったら送らない
// --- トークン(デモ用:user.署名)。本番は既存の認証(JWT・セッション)を使う
const sign = (user) => createHmac('sha256', SECRET).update(user).digest('base64url');
const issueToken = (user) => `${user}.${sign(user)}`;
function verifyToken(token) {
const [user, sig] = String(token ?? '').split('.');
if (!user || !sig) return null;
const a = Buffer.from(sig), b = Buffer.from(sign(user));
return a.length === b.length && timingSafeEqual(a, b) ? user : null;
}
// --- HTTP(静的ファイルとトークン発行)
const server = createServer(async (req, res) => {
const url = new URL(req.url, 'http://localhost');
if (url.pathname === '/token') { // デモ用。本番はログイン済みセッションから発行する
res.setHeader('content-type', 'application/json');
return res.end(JSON.stringify({ token: issueToken(url.searchParams.get('user') ?? 'guest') }));
}
if (url.pathname === '/ws') { // Upgrade ヘッダーなしで来たとき
res.writeHead(426, { 'content-type': 'text/plain' });
return res.end('Upgrade Required');
}
const file = url.pathname === '/reconnecting.js' ? 'reconnecting.js' : 'index.html';
const type = file.endsWith('.js') ? 'text/javascript' : 'text/html; charset=utf-8';
res.setHeader('content-type', type);
res.end(await readFile(new URL(`./public/${file}`, import.meta.url)));
});
// --- WebSocket(HTTP サーバーの upgrade で認証してから接続させる)
const wss = new WebSocketServer({ noServer: true, maxPayload: 64 * 1024 });
server.on('upgrade', (req, socket, head) => {
const url = new URL(req.url, 'http://localhost');
const reject = (code, text) => {
socket.write(`HTTP/1.1 ${code} ${text}\r\nConnection: close\r\n\r\n`);
socket.destroy();
};
if (url.pathname !== '/ws') return reject(404, 'Not Found');
const origin = req.headers.origin;
if (origin && !ALLOWED_ORIGINS.has(origin)) return reject(403, 'Forbidden');
const user = verifyToken(url.searchParams.get('token'));
if (!user) return reject(401, 'Unauthorized');
wss.handleUpgrade(req, socket, head, (ws) => wss.emit('connection', ws, req, user));
});
// --- ルーム管理
const rooms = new Map(); // room名 -> Set<WebSocket>
function join(ws, room) {
leave(ws);
if (!rooms.has(room)) rooms.set(room, new Set());
rooms.get(room).add(ws);
ws.room = room;
}
function leave(ws) {
const set = rooms.get(ws.room);
if (!set) return;
set.delete(ws);
if (set.size === 0) rooms.delete(ws.room);
ws.room = undefined;
}
// --- 送信(バックプレッシャー対策つき)
function safeSend(ws, obj) {
if (ws.readyState !== WebSocket.OPEN) return false;
if (ws.bufferedAmount > MAX_BUFFERED) { // 相手が受け取りきれていない
ws.dropped = (ws.dropped ?? 0) + 1;
return false;
}
ws.send(JSON.stringify(obj));
return true;
}
function toRoom(room, obj) {
for (const client of rooms.get(room) ?? []) safeSend(client, obj);
}
function toAll(obj) {
for (const client of wss.clients) safeSend(client, obj);
}
// --- 接続ごとの処理
wss.on('connection', (ws, req, user) => {
ws.user = user;
ws.isAlive = true;
ws.on('pong', () => { ws.isAlive = true; });
ws.on('error', (err) => console.error('ws error:', err.message));
console.log(`connected: ${user} (clients=${wss.clients.size})`);
safeSend(ws, { type: 'welcome', user });
ws.on('message', (data, isBinary) => {
let msg;
try {
if (isBinary) throw new Error('binary');
msg = JSON.parse(data.toString());
} catch {
return safeSend(ws, { type: 'error', code: 'BAD_JSON', message: 'JSON テキストで送ってください' });
}
switch (msg.type) {
case 'join':
if (typeof msg.room !== 'string' || !/^[\w-]{1,32}$/.test(msg.room)) {
return safeSend(ws, { type: 'error', code: 'BAD_ROOM', message: 'room は英数字32字まで' });
}
join(ws, msg.room);
safeSend(ws, { type: 'joined', room: msg.room, id: msg.id });
toRoom(msg.room, { type: 'system', room: msg.room, text: `${user} が入室しました` });
break;
case 'chat':
if (!ws.room) return safeSend(ws, { type: 'error', code: 'NOT_JOINED', message: '先に join してください' });
toRoom(ws.room, { type: 'chat', room: ws.room, from: user, text: String(msg.text).slice(0, 1000), ts: Date.now() });
break;
case 'announce':
toAll({ type: 'announce', from: user, text: String(msg.text).slice(0, 1000) });
break;
default:
safeSend(ws, { type: 'error', code: 'UNKNOWN_TYPE', message: `不明な type: ${msg.type}` });
}
});
ws.on('close', (code, reason) => {
leave(ws);
console.log(`closed: ${user} code=${code} reason=${reason} dropped=${ws.dropped ?? 0}`);
});
});
// --- ハートビート(ping に pong が返らない接続を切る)
const timer = setInterval(() => {
for (const ws of wss.clients) {
if (!ws.isAlive) {
console.log(`terminate (no pong): ${ws.user}`);
ws.terminate();
continue;
}
ws.isAlive = false;
ws.ping();
}
}, HEARTBEAT_MS);
wss.on('close', () => clearInterval(timer));
// --- 停止処理
function shutdown() {
for (const ws of wss.clients) ws.close(1001, 'server shutdown');
wss.close();
server.close(() => process.exit(0));
setTimeout(() => process.exit(0), 1000).unref();
}
process.on('SIGTERM', shutdown);
process.on('SIGINT', shutdown);
server.listen(PORT, () => console.log(`listening on http://localhost:${PORT}`));
noServer: true にして server.on('upgrade') で自分で受けるのが要点です。こうすると、WebSocket に切り替える前(HTTP の段階)でパス・Origin・トークンを確認し、不正な接続を 401 / 403 で断れます。
完成コード:自動再接続つきクライアント(ブラウザ・Node.js 共通)
public/reconnecting.js:
// reconnecting.js — ブラウザと Node.js 22(globalThis.WebSocket)の両方で動く
export function connect({ url, getUrl, onMessage, onOpen, log = console.log,
baseMs = 500, maxMs = 30000, maxRetries = Infinity }) {
let ws, attempt = 0, stopped = false, timer;
const queue = []; // 切断中に送ろうとしたメッセージ
// 指数バックオフ + フルジッター:0 〜 min(maxMs, baseMs * 2^attempt) のランダム
const backoff = (n) => Math.floor(Math.random() * Math.min(maxMs, baseMs * 2 ** n));
function scheduleRetry(code) {
log(`close code=${code}`);
if (stopped || code === 1008) return; // 1008 = ポリシー違反は再接続しない
if (attempt >= maxRetries) return log('give up');
const wait = backoff(attempt++);
log(`reconnect #${attempt} in ${wait}ms`);
timer = setTimeout(open, wait);
}
async function open() {
let target;
try {
target = getUrl ? await getUrl() : url; // 再接続のたびにトークンを取り直せる
} catch {
return scheduleRetry(1006); // トークン取得に失敗(サーバー停止中など)
}
const sock = (ws = new WebSocket(target));
let handled = false; // 1つの接続につき再接続の予約は1回
const onEnd = (code) => { if (!handled) { handled = true; scheduleRetry(code); } };
sock.onopen = () => {
attempt = 0;
log('open');
onOpen?.(api);
while (queue.length && sock.readyState === WebSocket.OPEN) sock.send(queue.shift());
};
sock.onmessage = (e) => onMessage?.(JSON.parse(e.data), api);
sock.onclose = (e) => onEnd(e.code);
// Node.js 22 ではハンドシェイク失敗時に close が来ないことがあるため error でも予約する
sock.onerror = () => { if (sock.readyState !== WebSocket.OPEN) onEnd(1006); };
}
const api = {
send(obj) {
const text = JSON.stringify(obj);
if (ws?.readyState === WebSocket.OPEN) ws.send(text);
else queue.push(text);
},
close() { stopped = true; clearTimeout(timer); ws?.close(1000, 'bye'); },
};
open();
return api;
}
指数バックオフ+フルジッターは、失敗するたびに待ち時間の上限を 0.5秒 → 1秒 → 2秒 … と倍にし、その範囲でランダムに待つ方法です。サーバー再起動のとき、全クライアントが同じ瞬間に再接続して負荷が集中するのを防げます。
chat-client.mjs(Node.js 22 から3人で接続するテスト):
// chat-client.mjs — Node.js 22 標準の WebSocket(import 不要)で動くクライアント
import { connect } from './public/reconnecting.js';
const BASE = process.env.BASE ?? 'http://localhost:7371';
const getUrl = async (user) => {
const { token } = await (await fetch(`${BASE}/token?user=${user}`)).json();
return `${BASE.replace('http', 'ws')}/ws?token=${encodeURIComponent(token)}`;
};
function member(user, room) {
return connect({
getUrl: () => getUrl(user),
log: (m) => console.log(`[${user}] ${m}`),
onOpen: (c) => c.send({ type: 'join', room, id: 1 }),
onMessage: (msg) => console.log(`[${user}] <-`, JSON.stringify(msg)),
});
}
const alice = member('alice', 'dev');
const bob = member('bob', 'dev');
const carol = member('carol', 'ops');
await new Promise((r) => setTimeout(r, 300));
alice.send({ type: 'chat', text: 'こんにちは' });
await new Promise((r) => setTimeout(r, 200));
carol.send({ type: 'announce', text: '全員へのお知らせ' });
bob.send({ type: 'dance' });
await new Promise((r) => setTimeout(r, 200));
[alice, bob, carol].forEach((c) => c.close());
実行結果:ブロードキャストとルーム
node server.mjs &
node chat-client.mjs
[bob] open
[bob] <- {"type":"welcome","user":"bob"}
(中略:alice・carol の接続と入室)
[bob] <- {"type":"chat","room":"dev","from":"alice","text":"こんにちは","ts":1790674741036}
[alice] <- {"type":"chat","room":"dev","from":"alice","text":"こんにちは","ts":1790674741036}
[bob] <- {"type":"announce","from":"carol","text":"全員へのお知らせ"}
[alice] <- {"type":"announce","from":"carol","text":"全員へのお知らせ"}
[carol] <- {"type":"announce","from":"carol","text":"全員へのお知らせ"}
[bob] <- {"type":"error","code":"UNKNOWN_TYPE","message":"不明な type: dance"}
[alice] close code=1000
[bob] close code=1000
[carol] close code=1000
alice の「こんにちは」は dev ルームの alice と bob にだけ届き、ops ルームの carol には届いていません。 announce は3人全員に届いています。
ブラウザから接続する(Playwright で確認)
public/index.html では、上の connect() をそのまま import し、getUrl の中で location.protocol が https: なら wss:// を組み立てます(const proto = location.protocol === 'https:' ? 'wss' : 'ws';)。
Playwright で2つのタブ(alice と bob)を開き、alice から送信したときの bob の画面の表示です。
bob の画面: [
'open',
'[welcome] ',
'[joined] dev',
'[system] bob が入室しました',
'alice: ブラウザから送信'
]
同じ reconnecting.js がブラウザと Node.js 22 の両方で動くのが、標準 WebSocket を使う利点です。
接続時の認証:upgrade でトークンを検証する
ブラウザの WebSocket は任意の HTTP ヘッダー(Authorization など)を付けられません。 そのため、次のどれかでトークンを渡します。
| 渡し方 | 例 | 注意点 |
|---|---|---|
| クエリ文字列 | /ws?token=... |
アクセスログに残るので 短い有効期限の専用トークン にする |
| Cookie | ログイン済みセッション Cookie | 同じサイトなら自動送信。Origin の確認を必ず行う |
| 最初のメッセージ | {"type":"auth","token":"..."} |
認証前のメッセージを受け付けない処理が必要 |
curl で upgrade リクエストを送って、各ケースの応答を確認しました。
--- トークンなし
HTTP/1.1 401 Unauthorized
--- 許可していない Origin
HTTP/1.1 403 Forbidden
--- 正しいトークン
HTTP/1.1 101 Switching Protocols
Upgrade: websocket
Connection: Upgrade
Sec-WebSocket-Accept: s3pPLMBiTxaQ9kYGzzhZRbK+xOo=
--- Upgrade ヘッダーなしで /ws に GET
HTTP/1.1 426 Upgrade Required
クライアントからは 401 の理由は見えません。 不正なトークンで接続したときのイベントは次のとおりでした。
| クライアント | 起きたイベント |
|---|---|
| Chromium 141 | error → close code=1006 wasClean=false |
| Node.js 22.22.2 | error(Received network error or non-101 status code.)のみ。close は発火せず、readyState は 0 のまま |
Node.js 22 では close が来ない場合があるため、上の reconnecting.js は error でも再接続を予約しています(1接続につき1回だけ)。「認証エラーだと画面に出したい」場合は、いったん接続を受け入れてから ws.close(1008, 'unauthorized') で閉じる方法もあります。
ハートビート:ping/pong で切れた接続を見つける
LAN ケーブルが抜けた、スマホが圏外になった、といった場合は TCP の切断がすぐには伝わらず、サーバーに「生きているように見える接続」が残ります。ws の README と同じ方法で、定期的に ping を送り、pong が返らない接続を terminate() します。pong は仕様上、受信側が自動で返すので、ブラウザ側のコードは不要です。
HEARTBEAT_MS=1000 で起動し、pong を返さないクライアント(ws クライアントの new WebSocket(url, { autoPong: false }))を混ぜて確認しました。
296ms alive: ping を受信(pong は自動で返す)
296ms silent: ping を受信(pong を返さない)
1295ms alive: ping を受信(pong は自動で返す)
1298ms silent: close code=1006
2296ms alive: ping を受信(pong は自動で返す)
3297ms alive: ping を受信(pong は自動で返す)
サーバー側にも terminate (no pong): silent と記録され、約1秒(1周期)で切断を検知できました。
間隔は、途中のプロキシのアイドルタイムアウトより短くします。 nginx の proxy_read_timeout の既定は 60秒なので、30秒間隔の ping なら切断されません。
自動再接続:サーバーを再起動して確かめる
クライアントの接続中にサーバーを止め、約3.5秒後に起動し直しました。切断中の 1.5秒時点で chat を送っています。
94ms open
95ms <- welcome
96ms <- joined
96ms <- system dave が入室しました
972ms close code=1001
972ms reconnect #1 in 212ms
1187ms close code=1006
1187ms reconnect #2 in 622ms
1811ms close code=1006
1811ms reconnect #3 in 1610ms
3423ms close code=1006
3423ms reconnect #4 in 2595ms
6035ms open
6039ms <- welcome
6040ms <- joined
6040ms <- system dave が入室しました
6040ms <- chat 切断中に送信
7078ms close code=1000
- 終了コードは 1000=正常、1001=Going Away、1006=close フレームなしの異常終了、1008=ポリシー違反
- サーバー停止時は
1001:shutdown()でws.close(1001, ...)を送っているためです。 - 待ち時間は上限 500 → 1000 → 2000 → 4000ms の範囲でランダム(212, 622, 1610, 2595ms)。
- 再接続後に
joinし直し、切断中に送ったchatもキューから送信されています。
ルームへの参加は接続ごとの状態なので、再接続したら onOpen で入り直す設計にしておくのがポイントです。
バックプレッシャー:bufferedAmount で送りすぎを防ぐ
bufferedAmount は 「send() したが、まだ OS に渡しきれていないバイト数」 です。相手の受信が遅いのに送り続けると、この値とサーバーのメモリ使用量が増え続けます。受信側が読めない状態を作り、64KB を 500回送った結果です。
// backpressure-demo.mjs(抜粋)
for (let i = 0; i < 500; i++) { // 64KB × 500 = 約31MB
if (mode === 'guarded' && ws.bufferedAmount > 1 * MB) { dropped++; continue; }
ws.send(chunk); sent++;
}
[naive] sent=500 dropped=0 bufferedAmount=27.6MB
[guarded] sent=75 dropped=425 bufferedAmount=1.0MB
閾値を設けると、滞留は約1MBで止まります。 捨ててよいデータ(カーソル位置や進捗率のように最新値だけ必要なもの)は捨て、捨てられないデータは「閾値を下回るまで待つ」「遅いクライアントを close(1013) で切る」などを選びます。
nginx のリバースプロキシ設定
WebSocket は HTTP/1.1 の Upgrade で始まるため、nginx では proxy_http_version 1.1 と Upgrade / Connection ヘッダーの転送が必要です。nginx 公式ドキュメントの map を使う書き方です。
http {
map $http_upgrade $connection_upgrade {
default upgrade;
'' close;
}
upstream chat_app { server 127.0.0.1:7371; }
server {
listen 7372; # 本番は listen 443 ssl; で wss:// にする
location / {
proxy_pass http://chat_app;
proxy_http_version 1.1;
proxy_set_header Host $host;
}
location /ws {
proxy_pass http://chat_app;
proxy_http_version 1.1;
proxy_set_header Upgrade $http_upgrade;
proxy_set_header Connection $connection_upgrade;
proxy_set_header Host $host;
proxy_set_header X-Forwarded-For $proxy_add_x_forwarded_for;
proxy_set_header X-Forwarded-Proto $scheme;
proxy_read_timeout 120s; # ハートビート間隔より長く
proxy_send_timeout 120s;
}
}
}
nginx 1.24.0 経由で接続した結果:
open via nginx
<- {"type":"welcome","user":"erin"}
<- {"type":"joined","room":"dev"}
<- {"type":"system","room":"dev","text":"erin が入室しました"}
Upgrade と Connection の2行を消すと、アプリには普通の GET として届きます。 今回のサーバーは 426 を返すので、原因がすぐわかります(トークンは途中を省略)。
error: Received network error or non-101 status code.
127.0.0.1 - - [29/Sep/2026:18:40:42 +0900] "GET /ws?token=erin.aOvG... HTTP/1.1" 426 27 "-" "node"
| nginx 設定 | 役割 |
|---|---|
proxy_http_version 1.1 + Upgrade / Connection |
Upgrade は HTTP/1.1 の仕組みで、2つのヘッダーは明示的な転送が必要 |
proxy_read_timeout |
無通信で切られる時間(既定 60秒)。ping 間隔より長くする |
| 複数台構成 | ルームを台をまたいで共有するなら Redis の Pub/Sub などで中継する |
よくある質問
Q. Node.js 18 や 20 でも標準の WebSocket は使えますか?
A. Node.js のドキュメントでは v20.10.0 / v21.0.0 で追加、v22.0.0 でフラグ不要、v22.4.0 で実験的機能ではなくなったとされています。安定版として使うなら Node.js 22 以降が目安です。それより前の環境ではクライアントにも ws を使えます(import WebSocket from 'ws')。
Q. トークンをクエリに入れても大丈夫ですか?
A. URL はアクセスログに残りやすいため、数十秒〜数分だけ有効な WebSocket 接続専用のトークンを発行するのがおすすめです。同じサイトなら Cookie + Origin 確認も選べます。この記事の再接続クライアントは、接続のたびに getUrl() でトークンを取り直します。
まとめ
- サーバーは ws、クライアントは Node.js 22 標準の
WebSocket(v22.4.0 で安定版) - メッセージは
type付き JSON。送信者・時刻・ルームはサーバーが付ける - 認証は
noServer+upgradeで検証し、不正なら 401 / 403 で断る - ハートビートは ping/pong。pong が返らない接続を
terminate() - 再接続は指数バックオフ+ジッター。Node.js 22 は
errorだけで終わる場合もある bufferedAmountの閾値で送りすぎを防ぐ(実測 27.6MB → 1.0MB)- nginx は
proxy_http_version 1.1と Upgrade / Connection の転送が必須
本記事の内容は、ご自由にお使いください。
ご利用の際は、出典として本ページへのリンクを記載いただけますようお願いします。
(記載例)出典:株式会社RJC「Node.js WebSocket実装|ws・再接続・認証・nginxまで」
株式会社RJCでは、リアルタイム通信のように動かして確かめると理解が深まる技術を、プロジェクトの中で仲間と学び合っています。こうした知識を共有しながら、日々技術を磨いています。
株式会社RJC ― SI事業・SES事業・AI駆動開発。RJCは一緒に成長を楽しめる会社です。
WE ARE HIRING
RJCで一緒に開発しながら、
成長を楽しみませんか?
RJCは、Web・モバイル・AIを活用した開発プロジェクトで、テックリードやPM・PMOも活躍するシステム開発会社です。会社を知る、待遇を確かめる、話を聞いてみる。気になるところ見てみてください!
- 127日年間休日
- 12時間平均残業時間
- 毎日ガチャ遊びココロも大切にする福利厚生。アマギフなどの賞品ラインナップ!
ほかにも、チケットレストラン、書籍読み放題、2年ごとの慰労報奨(休暇 or 金一封)、11期連続の黒字決算。