はじめに
Node.jsでWebSocketサーバーを立てるとき、真っ先に候補に挙がるのはSocket.IOやws、あるいはExpressやFastifyの組み合わせではないでしょうか。ただ、同時接続数が数万を超えるようなリアルタイムアプリケーションを作ろうとすると、これらのライブラリのオーバーヘッドがボトルネックになってくることがあります。
「もっと生々しく速いWebSocketサーバーが欲しい」——そんな要求に応えてくれるのがuWebSockets.jsです。C++で書かれたコアエンジンをNode.jsのネイティブアドオンとして公開しており、JavaScriptの書きやすさを保ちながらC++並みの速度を実現しています。
uWebSockets.jsとは
uWebSockets.js(略称: uWS)は、uNetworking社が開発しているHTTP/WebSocketサーバーライブラリです。約1万行のC++コードで実装されたコアを、V8のネイティブアドオンとしてNode.jsから呼び出せるようにしています。TechEmpower Benchmarksでは標準準拠の実装として最速クラスの結果を記録しており、次世代JavaScriptランタイムであるBunのHTTPサーバー実装のコア部分にも採用されています。
主な特徴
- 圧倒的な速度 - ベンチマークによってはSocket.IOの10倍以上、Fastifyの8.5倍程度のスループットを記録しています。C++でイベントループやメモリ管理を最適化しているため、GCの影響を受けにくいのが強みです。
- HTTP/WebSocketの統一API - 同じアプリケーションインスタンス上でHTTPルーティングとWebSocketのアップグレードを扱えるため、REST APIとリアルタイム通信を1つのサーバーにまとめられます。
- Pub/Sub機能を内蔵 - トピックベースのpublish/subscribeがライブラリレベルで用意されており、外部のメッセージブローカーなしでも簡単にブロードキャスト配信を組めます。
- 軽量なメモリフットプリント - 大量の同時接続を扱ってもメモリ使用量が抑えられるよう設計されており、チャットサービスやゲームサーバーのような常時接続型のワークロードに向いています。
インストール
uWebSockets.jsはnpmレジストリには登録されておらず、GitHubリポジトリから直接インストールする形式を取っています。
# npmの場合
npm install uNetworking/uWebSockets.js#v20.67.0
# yarnの場合
yarn add uNetworking/uWebSockets.js#v20.67.0
バージョンタグを指定することで、動作確認済みのバージョンを固定してインストールできます。プラットフォームごとにプリビルドバイナリが用意されているので、ビルド環境を用意する必要はありません。
基本的な使い方
まずは最小構成のHTTPサーバーを立ててみましょう。
const uWS = require('uWebSockets.js');
const app = uWS.App();
app.get('/*', (res, req) => {
res.writeHeader('Content-Type', 'text/plain; charset=utf-8');
res.end('Hello from uWebSockets.js!');
});
app.listen(9001, (listenSocket) => {
if (listenSocket) {
console.log('Listening on port 9001');
} else {
console.log('Failed to listen on port 9001');
}
});
ExpressのようなミドルウェアチェーンはありませんがGetやPost、Any、WsなどのメソッドをチェーンさせるシンプルなルーティングAPIになっています。
続いてWebSocketのエコーサーバーです。
const uWS = require('uWebSockets.js');
const decoder = new TextDecoder('utf-8');
const app = uWS.App();
app.ws('/*', {
// 圧縮設定
compression: uWS.SHARED_COMPRESSOR,
maxPayloadLength: 16 * 1024,
idleTimeout: 60,
open: (ws) => {
console.log('クライアントが接続しました');
},
message: (ws, message, isBinary) => {
// ArrayBufferをテキストにデコードしてそのまま送り返す
const text = decoder.decode(message);
ws.send(text, isBinary);
},
close: (ws, code, message) => {
console.log('クライアントが切断しました', code);
},
});
app.listen(9001, (listenSocket) => {
if (listenSocket) {
console.log('WebSocket server listening on port 9001');
}
});
messageハンドラで受け取るmessageはArrayBuffer型なので、テキストとして扱いたい場合はTextDecoderでデコードする点がwsライブラリなどとの違いです。
実践的なユースケース
チャットルームへのブロードキャスト配信
uWebSockets.jsの大きな強みの一つがPub/Sub機能です。トピックを購読させておけば、外部のPub/Subサーバーを用意せずにルーム単位のブロードキャストが実装できます。
const uWS = require('uWebSockets.js');
const decoder = new TextDecoder('utf-8');
const app = uWS.App();
app.ws('/chat/:room', {
idleTimeout: 120,
upgrade: (res, req, context) => {
const room = req.getParameter(0);
res.upgrade(
{ room },
req.getHeader('sec-websocket-key'),
req.getHeader('sec-websocket-protocol'),
req.getHeader('sec-websocket-extensions'),
context
);
},
open: (ws) => {
// 接続時にルームのトピックを購読
ws.subscribe(ws.room);
},
message: (ws, message, isBinary) => {
const text = decoder.decode(message);
// 簡易的なメッセージ長のバリデーション
if (text.length === 0 || text.length > 500) {
return;
}
// 同じルームを購読している全クライアントに配信
app.publish(ws.room, text, isBinary);
},
close: (ws) => {
ws.unsubscribe(ws.room);
},
});
app.listen(9001, () => {
console.log('Chat server listening on port 9001');
});
URLパスの:roomパラメータでルームを分け、subscribe/publishだけでルームごとのブロードキャストが実現できます。Redisなどの外部Pub/Subを別途構築しなくても、単一プロセス内であればこれだけでチャットサーバーの基盤が組めるのは大きな魅力です。
REST APIとWebSocketの共存
前述の通り、同じappインスタンス上にHTTPルートとWebSocketルートを共存させられます。
const uWS = require('uWebSockets.js');
const app = uWS.App();
app.get('/api/health', (res, req) => {
res.writeHeader('Content-Type', 'application/json');
res.end(JSON.stringify({ status: 'ok' }));
});
app.ws('/realtime', {
message: (ws, message, isBinary) => {
ws.send(message, isBinary);
},
});
app.listen(9001, () => {
console.log('API + WebSocket server listening on port 9001');
});
ヘルスチェック用のREST APIとリアルタイム通信用のWebSocketエンドポイントを1つのポート・1つのプロセスにまとめられるため、インフラ構成をシンプルに保てます。
まとめ
uWebSockets.jsは、C++実装ならではの速度とメモリ効率を武器に、大規模なリアルタイム通信を支えるための選択肢です。npmレジストリ非対応やExpress的なミドルウェア体系がないなど、既存のNode.jsエコシステムに慣れていると多少の癖を感じる部分もありますが、Pub/Sub機能を内蔵している点やHTTP/WebSocketを統一APIで扱える点は、実装のシンプルさという意味でも魅力的です。
同時接続数が課題になってきたチャットサービスやゲームサーバー、リアルタイムダッシュボードなどを開発している方は、一度ベンチマークを取って導入を検討してみる価値があるライブラリだと思います。