【API Gateway】WebSocket APIでNext.jsからつながってみた

AWS

こんばんは。

なんか今年は寒いですね。北海道は昼間も氷点下で、暖房と加湿器をフル稼働させてます❄️

今回は、とあるプロジェクトで複数人がリアルタイムに通信できる仕組みを作りたく、気になっていたAPI GatewayのWebSocket APIを試してみました。

フロント(Next.js)から接続するまでをゴールとします。

1. 必要なモノの確認

まず、なんといってもAPI GatewayのWebSocket APIです。

ルートとして、接続したとき($connect)、切断したとき($disconnect)、その他メッセージを受信したとき($default)を設定する必要があります。

また、それぞれに対応するLambda関数も用意します。ソケットは内部的に接続IDで管理されるので、メッセージ送信先を保持しておく必要があります。メジャーなのはDynamoDBですね。

2. Lambda

$connect

ソケットに接続されたときに呼ばれる関数です。

eventに接続IDが渡ってきますが、これを逃すともう送り先が分からなくなってしまうので、DynamoDBに保存するコードを書きます。

import boto3

def lambda_handler(event, context):
    # 接続ID
    connection_id = event['requestContext']['connectionId']

    # DynamoDBに保存
    boto3.client('dynamodb').put_item(~~)

    return {
        "statusCode": 200,
        "body": "こんにちは"
    }

$disconnect

切断時に呼ばれる関数です。

こちらも接続IDが渡ってきますが、切断した人に送ってもムダになるので、DynamoDBから消します。

import boto3

def lambda_handler(event, context):
    # 接続ID
    connection_id = event['requestContext']['connectionId']

    # DynamoDBから削除
    boto3.client('dynamodb').delete_item(~~)

    return {
        "statusCode": 200,
        "body": "さよなら"
    }

$default

メッセージ受信時に呼ばれる関数です。

つながってる人にメッセージを送るのはここでやります。

import json
import boto3

def lambda_handler(event, context):
    # イベントから接続IDとメッセージを取得
    body = json.loads(event['body'])
    message = body.get('message', '')

    # WebSocket用のエンドポイントURLを動的に取得(Lambda側からはhttp(s)で送るらしい)
    domain_name = event['requestContext']['domainName']
    endpoint_url = f"https://{domain_name}"

    # API Gateway Management APIクライアントを作成
    apigw = boto3.client('apigatewaymanagementapi', endpoint_url=endpoint_url)

    # すべての接続者にメッセージを送信
    # DynamoDBからqueryで検索する
    connection_ids = get_all_connection_ids()
    
    for conn_id in connection_ids:
        try:
            apigw.post_to_connection(
                ConnectionId=conn_id,
                Data=json.dumps({'message': message})
            )
        except apigw.exceptions.GoneException:
            # 既に接続が切れているクライアントには送信しない
            print(f"Connection {conn_id} is no longer active.")
    
    return {
        'statusCode': 200,
        'body': json.dumps('Message sent to all clients')
    }

3. API Gateway

今回は接続と切断以外は全て$defaultにしますが、メッセージの種類によってカスタムルートを定義するのがいいのかもですね。

作ったLambdaたちを割り当てます。

ステージを適当に作ったら、デプロイしておきましょう。

このままでも使えますが、API Gatewayから発行される wss://~~execute-api.ap-northeast-1.amazonaws.com というよくわからんURLは、サービスを世に出すうえでイヤなので、ちゃんとドメインを与えます。

あ、Route53で独自ドメイン、ACMでSSL証明書の作成はできてる前提で進めますね!!

では、カスタムドメインを設定していきます。

ドメイン名に設定したいドメインを入力して、ACMで作成したSSL証明書をセットします。

その後、APIマッピングで適当に作ったステージ名をセットすればOKです。

ちなみに、自分はプライベートで開発するときは、いつも「v1」です。なにかあればv1に上書きしてるので、「v2」は一生来ないですけどw

4. Route53

APIの準備が整ったので、いよいよドメインを与えます。

Aレコードで作成したAPI Gatewayをエイリアスでセットすれば完了です。

5. フロントからつながってみる

このプロジェクトのフロントはNext.jsで開発する予定なので、Next.jsからつながってみます。

"use client";

import { useState, useEffect } from "react";

export default function Home() {
  const [ws, setWs] = useState<WebSocket | null>(null);
  const [msg, setMsg] = useState<string>("");
  const [msgs, setMsgs] = useState<string[]>([]);

  useEffect(() => {
    const socket = new WebSocket("(ソケット通信APIのURL)");

    socket.onopen = () => {
      console.log("WebSocket connected");
    };

    socket.onmessage = event => {
      const data = JSON.parse(event.data);
      setMsgs(prevMsgs => [...prevMsgs, data.message]);
    };

    socket.onclose = () => {
      console.log("WebSocket disconnected");
    };

    setWs(socket);

    return () => {
      socket.close();
    };
  }, []);

  const sendMessage = () => {
    if (ws && ws.readyState === WebSocket.OPEN) {
      ws.send(JSON.stringify({ message: msg }));
      setMsg("");
    }
  };

  return (
    <div>
      <input type="text" value={msg} onChange={e => setMsg(e.target.value)} />
      <button type="submit" onClick={sendMessage}>
        送信
      </button>
      <ul>
        {msgs.map((m, index) => (
          <li key={index}>{m}</li>
        ))}
      </ul>
    </div>
  );
}

推しの子がついに完結しちゃいましたね。。

よし、いい感じ。ソケットはこれで決まり!!

Lambdaは月に100万コールまで無料で、API Gatewayも月に100万回のコールで1.26ドル、100万分の接続で0.315ドルとのことで、相当バズらないとほぼお金はかかりません。

24時間稼働のサービスだけど、サーバーを立てるのは個人的にはちょっともったいない感じがしてたので、これにてスッキリです⭐

ただ、Lambdaはコールドスタートをひいちゃうとやっぱりちょっと遅延してる感じがするので、なにか対策したい気も・・

$connectionのタイミングで$defaultのLambdaを1回叩くような小細工で解消してほしいけど・・しっかりやるなら「Provisioned Concurrency」が良いかもですね。

6. 参考

投稿者プロフィール

KatoShingo
学んだことをアウトプットしていきます!
好きなこと:音楽鑑賞🎵 / ドライブ🚗 / サウナ🧖

関連記事

  1. AWS

    【AWS】Github ActionsやAWS SAMを使ってAWS …

  2. AWS

    【AWS】Amazon DynamoDBを使ってみる(CLI、Part…

  3. AWS

    【AWS】Amazon DynamoDBを使ってみる(CLI、API)…

  4. AWS

    【API Gateway】Lambda Web AdapterからWe…

  5. AWS

    【AWS】Lambdaのバックアップ、復元

  6. AWS

    NuGetパッケージの管理で「このソースでは利用できません」と表示され…

最近の記事

  1. Node.js
  2. AWS

制作実績一覧

  1. Checkeys