WebSocket
なにかしらのWebアプリケーションを作る際に、ユーザー間でリアルタイムに情報交換が必要な場面があるかと思います。
そんな時にはWebSocketというプロトコルを使う事が定番です。JavaScript界隈では、WebSocketクライアントの機能は既にWebブラウザに組み込まれています。WebSocketサーバーを作成するためにはSocket.io
というパッケージを使う事が定番です。
Next.jsとWebSocket
Next.jsでは、サーバーサイドを作成することができますが、そこにWebSocketサーバーの機能を持たせたいなと思うのはごく自然な流れだと思います。
ところがNext.jsの新しい作り方であるApp Router
では現在のところWebSocketサーバーを組み込むことができません。筆者もそのやり方をいろいろと探しましたが、今のところそれは出来ないようです。
そこでNext.jsの昔の作り方であるPages Router
ではどうなのかと目を向けてみると、こちらの方はWebSocketサーバーを組み込むノウハウがいろいろとあるようです。幸いにもPages Router
とApp Router
は一つのNext.jsアプリケーション内で共存できますので、今回はWebSocketサーバーをPages Router
で作成し、残りの機能(クライアント側含む)はApp Router
で作成することにしましょう。
API Routes
Next.jsのPages Router
でサーバーサイドを作成するためにはAPI Routes
を利用します。API Routes
の基本のコードは下記のようになります。
import type { NextApiRequest, NextApiResponse } from 'next'
type ResponseData = {
message: string
}
export default function handler(
req: NextApiRequest,
res: NextApiResponse<ResponseData>
) {
res.status(200).json({ message: 'Hello from Next.js!' })
}
出展:https://nextjs.org/docs/pages/building-your-application/routing/api-routes
このコードが書かれたファイルをpages/api
フォルダ内に設置すると、/api/ファイル名
というURLでアクセスできるサーバーサイドが完成します。
今回の作戦に従い、ここにWebSocketサーバーを組み込みます。
Next.jsアプリケーションの雛形生成
Next.jsアプリケーションを準備しましょう。下記のコマンドを実行し、尋ねられる選択肢も下記のように選択してください。
> npx create-next-app@latest
✔ What is your project named? … next-rtc-app
✔ Would you like to use TypeScript? … Yes
✔ Would you like to use ESLint? … Yes
✔ Would you like to use Tailwind CSS? … Yes
✔ Would you like to use `src/` directory? … Yes
✔ Would you like to use App Router? (recommended) … Yes
✔ Would you like to customize the default import alias (@/*)? … No
途中の質問で「App Routerを使うか(Would you like to use App Router?)」と尋ねられますがこれをどう答えたとしても、所定のフォルダ(app
フォルダもしくはpages
フォルダ)さえ自作すれば、Pages Router
とApp Router
のどちらも利用できるようです。
Socket.ioのインストール
Socket.ioを利用するためのサーバー用とクライアント用のパッケージを同時にインストールしておきます。
> next-rtc-app
> npm install socket.io
> npm install socket.io-client
> npm install cors
WebSocketサーバーのコーディング
WebSocketサーバーのコードを完成させましょう。比較的、短いコードです。機能的には接続してきたWebSocketクライアントを受け入れ、接続状態を維持し、いずれかのWebSocketクライアントが送信してきたメッセージを、そのまま他のWebSocketクライアントに転送します。この際、メッセージの中身は特に見ていません。来たものをそのままムーディに受け流しているだけです。
下記のコードを作成する場所に注意してください。src
フォルダ内に、pages
フォルダ、そしてその中にapi
フォルダを作成してください。これらのフォルダの名前や階層構造はAPI Routes
でサーバーサイドを作成するために設定されていますので、変更はできません。
そのフォルダの中にsockets.ts
というファイルを作成します。こちらは任意の名前で構いませんが、クライアントからアクセスする際のURLに影響します。
ここまでで次のようにフォルダ構成が完成していればOKです。
src
└ pages
└ api
└ sockets.ts
下記のコードを書いてください。
import type { NextApiRequest, NextApiResponse } from 'next';
import cors from 'cors';
import type { Socket as NetSocket } from 'net';
import type { Server as HttpServer } from 'http';
import { Server as SocketServer } from 'socket.io';
// Next.jsの型定義を拡張してSocket.IOの型定義を追加
type ReseponseWebSocket = NextApiResponse & {
socket: NetSocket & { server: HttpServer & { io?: SocketServer } };
};
const corsMiddleware = cors();
// Next.jsのAPIルーティングの入り口となる関数
export default function SocketHandler(req: NextApiRequest, res: ReseponseWebSocket) {
if (req.method !== 'POST') {
return res.status(405).end();
}
if (res.socket.server.io) {
return res.send('already-set-up');
}
// Socket.IOのサーバーを作成する
const io = new SocketServer(res.socket.server, {
addTrailingSlash: false,
});
// クライアントが接続してきたら、コネクションを確立する
io.on('connection', (socket) => {
const clientId = socket.id;
console.log(`A client connected. ID: ${clientId}`);
// メッセージを受信したら、全クライアントに送信する
socket.on('message', (data) => {
io.emit('message', data);
console.log('Received message:', data);
});
// クライアントが切断した場合の処理
socket.on('disconnect', () => {
console.log('A client disconnected.');
});
});
// CORS対策を一応、有効にした上でサーバーを設定する
corsMiddleware(req, res, () => {
res.socket.server.io = io;
res.end();
});
}
WebSocketは、比較的単純なプロトコルで、connection
イベントで接続が確立し、message
イベントでメッセージが届きます。切断した場合はdisconnect
イベントが発生します。
API Routes
の仕組みとSocket.ioの仕組みをつなげる必要があります。API Routes
からのレスポンスを担当するNextApiResponse
型のres
引数に、Socket.ioの接続情報を持つオブジェクトをsocket.server.io
という名前のプロパティを組み込む事で実現します。
やっかいなのが、TypeScript的に型の整合性を持たせることです。NextApiResponse
型に、socket.server.io
という元々存在しないプロパティを勝手に増やそうとしても、TypeScriptはコンパイルエラーを出します。それはそうですね。
そこでTypeScriptの交差型 (Intersection Types)を利用してNextApiResponse
型にsocket.server.io
プロパティを結合します。
// Next.jsの型定義を拡張してSocket.IOの型定義を追加
type ReseponseWebSocket = NextApiResponse & {
socket: NetSocket & { server: HttpServer & { io?: SocketServer } };
};
交差型は&
で複数の型を結合できます。今回は、ioプロパティを持つオブジェクト型にserverプロパティを持つオブジェクト型を結合し、それをさらにsocketプロパティを持つオブジェクト型と結合し、最後にNextApiResponseと結合しています。(書いててわけわからんですね)
参考:https://qiita.com/suema0331/items/a145909db0bcbcc3f949
WebSocketを使って、メッセージを送信したい場合は、emit
メソッドを利用します。何を送るかは自由ですが、今回は(前述のとおり)クライアントから来たメッセージをそのまま送信しなおしているだけです。
// メッセージを受信したら、全クライアントに送信する
socket.on('message', (data) => {
io.emit('message', data);
console.log('Received message:', data);
});
最後に今回のコードでは、CORS(オリジン間リソース共有)を行うためのミドルウェア(リクエストとレスポンスの間で動作するフィルター的なもの)を利用しています。これはSocket.ioのバージョンによっては必要なので入れてあります。ただし現時点で最新版のSocket.io 4.7.2
では不要なようです。
以上でコードの説明は完了です。
WebSocketサーバーの起動
WebSocketサーバーの起動は、Next.jsアプリケーションを起動するためのコマンドと同じです。これでサーバーとクライアントの双方を起動できます。
npm run dev
クライアント側の作成
次の記事で説明します:https://qiita.com/ochiochi/items/102d14649396d351ab80
参考情報
当記事は下記の記事を参考にしています。すごく助かりました。ありがとうございました。
https://www.zenryoku-kun.com/new-post/nextjs-socketio