カテゴリー
【AWSで構築するサーバレスWebSocket①】 AWS APIGatewayからWebSocket APIを試してみる
※ 当ページには【広告/PR】を含む場合があります。
2021/07/22

以前、別ブログでやっていたRestAPIを手動で構築する方法を紹介していました。
今回はRest APIではなく別の選択肢としてもっとも簡単なWebSocket APIの作成にチャレンジしていきます。
手動でWebSocket APIを構築する

今回は上の図のようなもっともシンプルなWebSocket APIを利用したチャットアプリのようなものをAPI Gatewayで構築してみましょう。
WebSocket APIルート
特定のリクエストで使用するルートを決定する際に利用する、
ルートセレクションエクスプレッション(ルート選択式)
このルート選択式とは、クライアントから送られるリクエストに対して、
ルート
routeKey
同じルートで接続したクライアント間に対して双方向通信が確立するようにできます。
API Gatewayが内部で定義しているrouteKeyには、自分で定義して使うルート以外に、以下の特別な3つルートがあります。
$default :
RSXがAPIルート内の他のrouteKeyに一致しない場合に使用される。
例としてはエラー処理などを実装するときに利用する
$connect :
クライアントが最初に接続するときの処理に使うルート
$disconnect :
クライアントが切断する処理に使うルート。
実装は任意で、切断処理をカスタマイズする場合に利用
また、自分で定義しているルートはリクエストボディのコンテクストから指定します。
デフォルトではユーザー定義のrouteKeyは
$request.body.action
Websocket APIの処理の大まかな流れとしては、
1. WebSocket API接続時にクライアントを情報をユーザーとして登録
2. 接続後に提供されるコールバックURLを使用して特定のユーザーにメッセージを送信
3. 接続確立後にユーザーは相互にメッセージを送受信する
4. ユーザーが切断後は登録情報を削除する
というフローになると思います。
ルートハンドラの実装
まずは先にLambdaのダッシュボードからWebSocket APIの各ルートの使うハンドラ関数から仕込んでいきます。
いかのように4つのLambda関数を新規作成します。
+ hello-websocket-onconnect :
$connectルートに対応。
接続時の初期化処理
+ hello-websocket-ondisconnect :
$disconnectルートに対応。
切断時の後処理
+ hello-websocket-onerror :
$defaultルートに対応。
接続中のエラーハンドリング
+ hello-websocket-onmessage :
カスタムルートに対応。
接続中のクライアント間メッセージを転送
Lambdaの新規作成の手順の説明は簡略化しますが、以下の図のようにシンプルなHello Worldテンプレートから作成した関数のindex.jsを編集してDeployするだけです。

では作成した4つのLambdaのindex.jsの中身を後述していきます。
WebSocket APIのアクセス権限
ハンドラ関数の実行ロールでアクセス権限が不足しているとWebSocket APIが実行できない場合があります。

Lambdaを実行する
AWSLambdaBasicExecutionRole
AmazonAPIGatewayInvokeFullAccess
execute-api:ManageConnections
$connectイベント用のハンドラ
$connect
$disconnect
例えばonConnect関数は、requestContextでユーザーから送られてくる登録情報を元に、データベースにレコードする処理を行うことができます。
$connect
今回はただのステータスコードを返すだけにしておきます。
exports.handler = async (event, context, callback) => {
//...何か接続開始処理を行う
try {
return {
statusCode: 200,
body: 'Connected! Hello WebSocket App!',
};
} catch(e) {
return {
statusCode: 500,
body: `Failed to connect: ${JSON.stringify(e)}`,
};
}
};
$disconnectイベント用のハンドラ
通常このイベントでは、切断されたクライアントの情報を元に、DynamoDBなどでレコードされた値を削除するような処理を割り当てます。
さきほどの
$disconnect
exports.handler = async (event, context, callback) => {
//...何か切断処理を行う
try {
return {
statusCode: 200,
body: 'Disconnected! Good-bye WebSocket App!',
};
} catch(e) {
return {
statusCode: 500,
body: `Failed to disconnect: ${JSON.stringify(e)}`,
};
}
};
$defaultイベント用のハンドラ
このイベントは
$connect
$disconnect
このハンドラ関数の実装は任意です。
exports.handler = async (event, context, callback) => {
//...何かのルート解析処理を行う
return {
statusCode: 200,
body: 'Undefined Route Detected!',
};
};
今回は具体的なルートエラーハンドリングの実装は説明しませんがとりあえず仕込んでおきます。
ユーザー定義イベント用のハンドラ
WebSocket API作成でもっとも重要なものが、ユーザー定義ルートのハンドリングです。
ここでは例として
sendMessageRoute
このハンドラ関数は、リクエストで送られてきたJSON形式のメッセージオブジェクトから
request.body.action
sendMessageRoute
WebSocket APIに接続の確立したWebCleintクライアントは、以下のJSON形式の文字列をサーバーに対し送信することで通信を行います。
{
"action": "sendMessageRoute",// ルート変数は必須
"data": "<送信したいデータ(UTF8)>",
"arg1": "<ユーザー定義変数1>",
"arg2": "<ユーザー定義変数2>",
...
}
このJSONデータではrouteKeyとして設定した変数である
action
このWebSocketクライアントから送られてきた文字列をハンドラ関数のイベントのリクエストコンテクスト(requestContext)で受け取ることでLambda側の処理が可能です。
今回の処理手順としては主に以下のようになります。
1. WebSocket通信するためApiGatewayManagementApiインスタンスを生成。
コンストラクター引数ではendpointが必要となり、
その際にrequestContextからdomainNameとstageを利用する
2. ハンドラ関数のevent引数に渡されるRequestContextからconnectionIdを取得
3. WebSocketクライアントからのメッセージがforwardが空でなく、
指定先のクライアントのconnectionIdがあった場合には、
メッセージをそのクライアントに転送先を変更する。
forwardが空だった場合には、自分にメッセージを送る
4. ApiGatewayManagementApiインスタンスから、
postToConnectionメソッドで指定のクライアントにメッセージを送信する
ここでWebSocket APIの機能の中核を担うのが
では先程の処理手順1-4に沿って、クライアント間でメッセージをやり取りするだけのハンドラ関数の実装を以下に示します。
const AWS = require('aws-sdk');
AWS.config.update({ region: process.env.AWS_REGION });
require('aws-sdk/clients/apigatewaymanagementapi');
exports.handler = async (event, context, callback) => {
const apigwManagementApi = new AWS.ApiGatewayManagementApi({
apiVersion: "2018-11-29",
endpoint: event.requestContext.domainName + "/" + event.requestContext.stage
});
const forward_id = JSON.parse(event.body).forward;
const ConnectionId = forward_id != null ? forward_id : event.requestContext.connectionId;
const Data = forward_id != null ? `${JSON.parse(event.body).data}` : `YOUR ID: ${ConnectionId}`;
const postParams = { ConnectionId, Data };
apigwManagementApi.postToConnection(postParams, (err) => {
if (err) {
console.log("Failed to post. Error: " + JSON.stringify(err));
}
});
callback(null, {
statusCode: 200,
body: "Send data!"
});
};
単にクライアントの接続IDを指定してメッセージを投げるだけのものですが、処理の初歩として理解するには良い題材です。
では、ここまでで各ルートに対するLambdaハンドラの下準備を終えましたので、API Gatewayの準備の手順を以下で説明していきます。
API Gatewayインスタンスの作成
API GatewayのダッシュボードからWebSocket APIを新規作成します。

まずはAPI名を
wsMyApp
またルート選択式の引き出し変数を
request.body.action

次のページでWebSocket APIで必要なルートを選択追加します。

先行して作成していたLambdaハンドラ関数を各ルートイベントに割り当てます。

ステージ名は任意です。

内容を確認したらデプロイを実行します。

問題がなければ、API Gatewayインスタンスのデプロイが成功しています。

2つのクライアント間での接続確認
それでは先ほど作ったWebSocket APIの動作確認をやっていきます。
wscatを使ってコンソールで確認
node.js環境下で動作しますので、以下のコマンドでインストールすると即時利用可能です。
$ npm install -g wscat
wscatコマンドからWebSocket APIを呼び出すためには、先程作ったAPI Gateway側のエンドポイントにアクセスする必要があります。

2つのWebSocketクライアントから呼び出せるように、ここでターミナルを二つ開いておきましょう。
1つ目のターミナルで早速WebSocket API側に接続してみます。
$ wscat -c wss://[APIのドメインID].[APIのリージョン名].amazonaws.com/[APIをデプロイしたステージ名]
Connected (press CTRL+C to quit)
#👇forwardフィールド無しでsendMessageRouteへシグナル送信
> {"action":"sendMessageRoute", "data":"who am i"}
< YOUR ID: C51DherHNjMCI3Q=
本来は接続完了時にWebSocketクライアントのconnectionId値を格納するデータベース(dynamoDBなど)をバックエンドで保管・管理するやり方が普通です。 今回はテストということで敢えて相手方の接続IDを直接入力するやり方で双方向通信をやっています。
また別のターミナルでもWebSocketクライアントを立ち上げてみます。
$ wscat -c wss://[APIのドメインID].[APIのリージョン名].amazonaws.com/[APIをデプロイしたステージ名]
Connected (press CTRL+C to quit)
> {"action":"sendMessageRoute", "data":"who am i"}
< YOUR ID: C50a8cvrtjMCI7w=
最初のクライアントとはまた別の接続IDが得られたことが分かります。
では、最初のクライアント(wscat1)から2つ目のクライアント(wscat2)へメッセージを送信してみます。
wscat1のターミナルでwscat2の接続IDを指定し、dataを送信してみましょう。
> {"action":"sendMessageRoute", "data":"Hello wscat2!", "forward":"C50a8cvrtjMCI7w="}
するとwscat2のターミナルでは
< Hello wscat2!
と表示されているならば成功です。
逆にwscat2からwscat1に以下を送信してみます。
> {"action":"sendMessageRoute", "data":"Hello wscat1!", "forward":"C51DherHNjMCI3Q="}
今度はwscat1のターミナルに
< Hello wscat1!
という表示があれば相互に通信が確立していることが分かります。

ブラウザで確認
ブラウザ側でWebSocketクライアントとして利用するためにはJavascriptスクリプトから
ここでは実装の方針だけを掻い摘むと、以下のような使い方になるでしょう。
//WebSocket APIでインスタンスを生成
const ws = new WebSocket("wss://[APIのドメインID].[APIのリージョン名].amazonaws.com/[APIをデプロイしたステージ名]")
//メッセージの受信のためのコールバック
ws.onmessage = (e) => {
const data = JSON.parse(e.data);
// ...
}
//定義済みルートへメッセージ送信するメソッド
const message = {
"action": "sendMessageRoute",
"data": "送信メッセージ",
// ...
}
ws.send(JSON.stringify(message))
ユーザーにブラウザからアクセスさせて使う場合、わざわざ別のクライアントの接続IDを指定させて利用させるのは不便です。
このためバックエンドでクライアント情報を管理できるデータベースをセットでWebSocketアプリケーションを構築する必要が出てきます。
まとめ
今回はAPI Gateway上でもっとも簡単なWebSocket APIを手動で構築する手順を詳しく解説していきました。
次回以降では、今回行った手動によるAPI構築法を、Serverless Frameworkで置き換えるための内容を解説していこうかと思います。
参考サイト
記事を書いた人
ナンデモ系エンジニア
主にAngularでフロントエンド開発することが多いです。 開発環境はLinuxメインで進めているので、シェルコマンドも多用しております。 コツコツとプログラミングするのが好きな人間です。
カテゴリー