21
12

Delete article

Deleted articles cannot be recovered.

Draft of this article would be also deleted.

Are you sure you want to delete this article?

カサレアルAdvent Calendar 2023

Day 19

Next.jsでWebSocketアプリケーションを作成する(サーバー編)

Last updated at Posted at 2023-12-25

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 RouterApp 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 RouterApp 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

下記のコードを書いてください。

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

21
12
0

Register as a new user and use Qiita more conveniently

  1. You get articles that match your needs
  2. You can efficiently read back useful information
  3. You can use dark theme
What you can do with signing up
21
12

Delete article

Deleted articles cannot be recovered.

Draft of this article would be also deleted.

Are you sure you want to delete this article?