314
256

Delete article

Deleted articles cannot be recovered.

Draft of this article would be also deleted.

Are you sure you want to delete this article?

More than 3 years have passed since last update.

APIGatewayでWebSocketが利用可能になったのでチャットAPIを構築してみた

Last updated at Posted at 2018-12-19

はじめに

本日からAPIGatewayでWebSocket APIを構築することができるようになりました!!
従来、WebSocket通信ができるアプリケーションやAPIを構築するためにはWebsocket通信の接続を管理するホストとなるサーバを立てておく必要がありました。

よくやる構成としては以下のようなものだったのではないでしょうか?

image.png

アクセスはALBで負荷分散したいが、EC2にステートを持たせられないのでWebSocket通信の接続情報はElasticCacheなどに退避させておくことでスケールする環境をなんとか作るといった具合でしょうか。

この構成は安定した作りではあるものの、個人的には

image.png

の3重苦で、あまり好きではないです。昔苦労したなぁ・・・
そこで待望のAPIGatewayがWebSocketをサポートした(2018年12月19日から)とのことらしいので早速使ってみようと思います。ワクワク🎵

APIGatewayでWebSocketの設定をしてみる

APIGatewayでWebSocket APIを構築することで、双方向通信ができるアプリケーションを簡単に実装することができるようになります!
WebSocket APIを使うことでチャットや双方向動画配信などが可能になり、Webアプリケーションの可能性が広がりますね!!

まずはじめにAPIGatewayのコンソール画面をのぞいてみましょう。
APIの作成から新規にAPIGatwayを作成しようとすると通信プロトコルをRESTにするかWebSocketにするかが選べます。

image.png

WebSocketにチェックを入れるとAPI名、Route Selection Expression、説明の入力が求められます。
Route Selection Expressionという項目は初登場ですね。

RouteSelectionExpression
$request.body.action

のように入力しておきましょう。

クライアントからAPIを送信する際にリクエストメッセージは以下のようなJSON形式で送ります。

リクエストメッセージ
{
    "service" : "chat",
    "action" : "join",
    "data" : {
        "room" : "room1234"
   }
}

serviceはrouteKey、評価された値と正確に一致するルートを使用します。Route Selection Expression$request.body.action として設定しておくと、actionプロパティに基づいてAPIの動作を選択することができます。あとはdataが実際に送られるメッセージデータですね。

ルートとは?

WebSocket APIはルートという単位で構成されています。RESTの場合はリソースパスでしたね。特定のリクエストで使用するルートをどう使うか?という設定を**Route Selection Expression(選択式)**にて行います。式はルートのrouteKeyの値に対応するものに接続されます。

3つの特別な
routeKey値
説明
$default 選択式がAPIルート内の他のrouteKeyに
一致しない値を生成する場合に使う
$connect クライアントがWebSocket APIに
最初に接続するときに使う
$disconnect クライアントがAPIから切断するときに使う

詳細はこちらに記載されています。

チャットAPIを作ってみる

今回作成する構成はこちらです。ユーザはOnConnect Lambdaを叩いて接続します。SendMessage Lambdaはユーザからのメッセージ送信を受け取ると、このWebSocket通信で繋がるすべてのユーザにPUSHします。OnDisconnect Lambdaを叩いてWebSocket通信を切断します。接続情報はDynamoDBに保持することでLambdaをステートレスに保ちます。LambdaはAWSからサンプルとして提供されているこちらのServerlessApplicationRepositoryのアプリを使用します。

image.png

サンプルアプリケーション(Lambda)のデプロイ

まずはServerlessApplicationRepositoryのアプリを自分のAWS上にデプロイします。

bash
# 今回使用するアプリケーションのリポジトリをクローン
$ git clone https://github.com/aws-samples/simple-websockets-chat-app.git
# クローンしてきたディレクトリに移動
$ cd simple-websockets-chat-app
# SAMテンプレートをpackageコマンドを使用してLambdaのソースコードと共にS3に格納
$ sam package --template-file template.yaml --output-template-file output.yaml --s3-bucket <自分のS3バケット>
# デプロイ
$ aws cloudformation deploy --template-file output.yaml --stack-name  <スタック名>  --capabilities CAPABILITY_IAM

今回はスタック名 websocket としてデプロイしました。

image.png

Lambdaが3種類
image.png

DynamoDBは simplechat_connections という名前で出来上がります。
image.png

APIGatewayの設定

さて、いよいよAPIGatewayの設定を進めていきます。
構成図どおり、3つのルートを作成します。まずはsendmessageルートから作成しましょう。

New Route Keyにsendmessageを入力して確定します。
あとはこのルートとLambda関数を接続しましょう。

image.png

SendMessage Lambda

ここでSendMessage Lambda関数に注目してみます。
ユーザの誰かが “sendmessage”アクションでメッセージを送信すると、
DynamoDBテーブルから、現在接続されているすべてのユーザを検索します。
検索されたすべてのユーザに対して、postToConnectionを呼ぶことでメッセージを送信します。

sendmessage/app.js
// Copyright 2018 Amazon.com, Inc. or its affiliates. All Rights Reserved.
// SPDX-License-Identifier: MIT-0

var AWS = require('aws-sdk');
AWS.config.update({ region: process.env.AWS_REGION });
var DDB = new AWS.DynamoDB({ apiVersion: "2012-10-08" });

require('aws-sdk/clients/apigatewaymanagementapi');

exports.handler = function (event, context, callback) {
  var scanParams = {
    TableName: process.env.TABLE_NAME,
    ProjectionExpression: "connectionId"
  };

  DDB.scan(scanParams, function (err, data) {
    if (err) {
      callback(null, {
        statusCode: 500,
        body: JSON.stringify(err)
      });
    } else {
      var apigwManagementApi = new AWS.ApiGatewayManagementApi({
        apiVersion: "2018-11-29",
        endpoint: event.requestContext.domainName + "/" + event.requestContext.stage
      });
      var postParams = {
        Data: JSON.parse(event.body).data
      };
      var count = 0;

      data.Items.forEach(function (element) {
        // ユーザの誰かが “sendmessage”アクションでメッセージを送信すると、
        // DynamoDBテーブルから、現在接続されているすべてのユーザを検索します。 
        // 検索されたすべてのユーザに対して、postToConnectionを呼ぶことでメッセージを送信します。
        postParams.ConnectionId = element.connectionId.S;
        apigwManagementApi.postToConnection(postParams, function (err) {
          if (err) {
            if (err.statusCode === 410) {
              console.log("Found stale connection, deleting " + postParams.connectionId);
              DDB.deleteItem({ TableName: process.env.TABLE_NAME,
                               Key: { connectionId: { S: postParams.connectionId } } });
            } else {
              console.log("Failed to post. Error: " + JSON.stringify(err));
            }
          } else {
            count++;
          }
        });
      });

      callback(null, {
        statusCode: 200,
        body: "Data send to " + count + " connection" + (count === 1 ? "" : "s")
      });
    }
  });
};

OnConnect Lambda

次にWebSocket通信を開始するLambdaです。APIGatewayの設定は**$connect**ルートに接続させるようにします。

image.png

関数の中身は非常にシンプルです。リクエストメッセージ(requestContext)のconnectionId値をDynamoDBテーブルに保存します。

onconnect/app.js

// Copyright 2018 Amazon.com, Inc. or its affiliates. All Rights Reserved.
// SPDX-License-Identifier: MIT-0

var AWS = require("aws-sdk");
AWS.config.update({ region: process.env.AWS_REGION });
var DDB = new AWS.DynamoDB({ apiVersion: "2012-10-08" });

exports.handler = function (event, context, callback) {
  // リクエストメッセージからconnectionIdを取得して
  var putParams = {
    TableName: process.env.TABLE_NAME,
    Item: {
      connectionId: { S: event.requestContext.connectionId }
    }
  };

  // DynamoDBテーブルに保存する
  DDB.putItem(putParams, function (err) {
    callback(null, {
      statusCode: err ? 500 : 200,
      body: err ? "Failed to connect: " + JSON.stringify(err) : "Connected."
    });
  });
};

OnDisConnect Lambda

image.png

こちらもシンプルですね。OnConnectと真逆の処理になります。DynamoDBに保持してあるデータを削除するだけです。

ondisconnect/app.js
// Copyright 2018 Amazon.com, Inc. or its affiliates. All Rights Reserved.
// SPDX-License-Identifier: MIT-0

var AWS = require("aws-sdk");
AWS.config.update({ region: process.env.AWS_REGION });
var DDB = new AWS.DynamoDB({ apiVersion: "2012-10-08" });

exports.handler = function (event, context, callback) {
  // リクエストメッセージからconnectionIdを取得して
  var deleteParams = {
    TableName: process.env.TABLE_NAME,
    Key: {
      connectionId: { S: event.requestContext.connectionId }
    }
  };
  
  // DynamoDBのテーブルから削除
  DDB.deleteItem(deleteParams, function (err) {
    callback(null, {
      statusCode: err ? 500 : 200,
      body: err ? "Failed to disconnect: " + JSON.stringify(err) : "Disconnected."
    });
  });
};

ステージへのデプロイ

ここは従来の方法と変わらないようです。

Action>Deploy APIを押すと設定画面が立ち上がりますので
image.png

デプロイ先のステージ名などを入力します。ここではprodステージにデプロイします。
image.png

ステージが正常にデプロイされるとWebSocket URLとConnection URLが発行されます。これで構築作業は完了です。

image.png

Websocketを体感する(テストしてみる)

ここまでできたら生成されたWebSocket URLに対してwssコネクションを貼りましょう。

コマンドラインツール の wscat を使って検証してみます。

bash
# wscatコマンドのインストール
$ npm install -g wscat
# APIGateayのステージエンドポイントにwssコネクションを貼る
$ wscat -c wss://{YOUR-API-ID}.execute-api.{YOUR-REGION}.amazonaws.com/prod
connected (press CTRL+C to quit)

# メッセージを送信
> {"action":"sendmessage", "data":"hello world"}
< hello world

双方向通信の様子

コンソールを複数開いてメッセージを送信すると、すべてのコンソールにメッセージがPUSHされていることがわかります。
下の動画ではで真ん中と上のコンソールがwscatコマンドでWebSocket通信を行なっている様子が見て取れます。一番下のコンソールはWebSocket通信を開始すると、DynamoDBにconnectionIdが保存されてWebSocket通信を切断するとconnectionIdが破棄される様子を観測しています。

websocket.gif

さいごに

いかがだったでしょうか?今回はAPIGatewayの新機能であるWebSocket通信のサポートを細かく追って解説してみました。
これでフルサーバレスでのWebSocket通信が可能になります。チャットでも双方向動画通信でもゲームでも!活躍する機会はどんどん出てきそうですね!!活用していきましょう:muscle:

314
256
2

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
314
256

Delete article

Deleted articles cannot be recovered.

Draft of this article would be also deleted.

Are you sure you want to delete this article?