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期連続の黒字決算。

ABOUT RJC RJCがどんな会社か知る 考え方、研修、働き方、福利厚生、社員の前職まで。RJCのことが丸わかり! RJC丸わかりページへ JOB DESCRIPTION 仕事内容・待遇を見てみる 仕事内容、給与・待遇、選考の流れ。経験者も未経験も!応募前に知りたいこと、まとめました! 募集要項を見る ENTRY エントリーする エントリーは1〜2分・履歴書不要です。まずは話を聞いてみたい、という方でも歓迎です! エントリーフォームへ

RJCで一緒に開発しながら、 成長を楽しみませんか?