はじめに
はじめまして。ニフティ株式会社の森田です。前に社内の1dayハッカソンに参加して、WebSocketを利用して用意した音声を同時共有する簡易アプリを作りました。AWSの勉強も兼ねて色々なサイトやサンプルを参考にして、改めて似たような簡易アプリを個人的に作ってみたのでその紹介をしたいと思います。
WebSocketとはクライアント・サーバ間で対話的な通信ができる技術です。HTTPを使ってソケット通信ができるイメージです。スマート家電とかでも使われていると聞きます。今回は、WebSocketを使ってコネクション接続中の全てのクライアントに画像(正確には画像保存先のS3 URL)を共有するアプリをAWSで作成してみました。
構成
AWS構成図はこんな感じです。
AWS API Gateway
API GatewayではWebSocket APIも作成できるため、サーバレスでの双方向通信が割と簡単に実現できます。REST APIではパスやメソッドごとにリソースを用意して、それにLambdaなどのサービスを統合します。WebSocket APIでは、接続時 ($connect) や切断時 ($disconnect) などのリソース(ルート)が最初から用意されています。最初から用意されているもの以外は別にルートを作成して、ルート選択式でそのルートに向けれらるようにします。それら各ルートに対してLambdaなどのサービスを統合するという基本的な流れは同じです。
API Gateway での WebSocket API についてhttps://docs.aws.amazon.com/ja_jp/apigateway/latest/developerguide/apigateway-websocket-api-overview.html
AWS Lambda
WebSocket接続、切断、など色々なタイミングで、Lambdaに書かれたコードで実際に処理されます。そうするために関数ごとにAPI Gatewayとの統合が必要となります。WebSocket APIで言うと、$connectルートに統合したLambda関数は接続時に動作します。このようにルートごとにLambda関数と統合させます。
AWS DynamoDB
API GatewayでWebSocketのサポートをしてくれるとはいえ、コネクションIDの管理は別でやらないといけません。今回はDynamoDBで1つテーブルを作成して、接続してきたコネクションのIDを雑に保存します。接続時にはIDをINSERTして、切断時にはIDを物理削除します。テーブルにIDがあればコネクションが接続中と判断します。
AWS S3
画像の保存用。今回、クライアント側で画像表示をするため、オブジェクトのURLをWebSocketでやり取りするようにします。S3で用意したバケットのバケットポリシーを変更して、外部からアクセスできるようにする必要もあります。今回は以下の3つの画像を用意しました。「いらすとや」の画像を使わせていただいています。
詳細
実際のデプロイについては最後にまとめています。Lambdaの処理内容の説明では、WebSocketを利用した時の処理の雰囲気が伝われば良いなと思います。
Lambda 接続時(onconnect) 処理内容
接続してきたコネクションのコネクションIDをDynamoDBに保存しています。Lambda関数のコードは以下です。
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 |
import os import logging import boto3 logger = logging.getLogger() logger.setLevel(logging.INFO) CONNECTION_TABLE = os.environ['CONNECTION_TABLE'] def lambda_handler(event, context): dynamodb = boto3.resource('dynamodb') connection_table = dynamodb.Table(CONNECTION_TABLE) connection_id = event.get('requestContext', {}).get('connectionId') try: # コネクションIDをDynamoDBに保存する connection_table.put_item(Item={'connectionId': connection_id}) logger.info(f'connected id: {connection_id}') except Exception as e: logger.error(e) return {'statusCode': 500, 'body': f'Failed to connect: {e}'} return {'statusCode': 200, 'body': 'Connected.'} |
Lambda 切断時(disconnect) 処理内容
切断したコネクションのコネクションIDをDynamoDBから物理削除します。Lambda関数のコードは以下です。
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 |
import os import logging import boto3 logger = logging.getLogger() logger.setLevel(logging.INFO) CONNECTION_TABLE = os.environ['CONNECTION_TABLE'] def lambda_handler(event, context): dynamodb = boto3.resource('dynamodb') connection_table = dynamodb.Table(CONNECTION_TABLE) connection_id = event.get('requestContext', {}).get('connectionId') try: # コネクションIDをDynamoDBから削除する connection_table.delete_item(Key={'connectionId': connection_id}) logger.info(f'disconnect id: {connection_id}') except Exception as e: logger.error(e) return {'statusCode': 500, 'body': f'Failed to disconnect: {e}'} return {'statusCode': 200, 'body': 'Disconnected.'} |
Lambda 画像共有(sendimage) 処理内容
11行目のIMAGE_S3_BUCKET_NAMEには画像保存用に作成したバケット名を記述してください。
クライアントからリクエスト時に送られた選択画像名(selectedImage)から画像を保存しているS3 URLを構成して、コネクション接続中のクライアント(DynamoDBに保存されてる全てのコネクションID)にそのURLを送る処理をします。Lambda関数のコードは以下です。
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 |
import json import os import logging import boto3 logger = logging.getLogger() logger.setLevel(logging.INFO) CONNECTION_TABLE = os.environ['CONNECTION_TABLE'] IMAGE_S3_BUCKET_NAME = '' S3_ENDPOINT_URL = f'https://{IMAGE_S3_BUCKET_NAME}.s3.ap-northeast-1.amazonaws.com' def lambda_handler(event, context): dynamodb = boto3.resource('dynamodb') connection_table = dynamodb.Table(CONNECTION_TABLE) DOMAIN_NAME = event['requestContext']['domainName'] STAGE = event['requestContext']['stage'] WEBSOCKET_ENDPOINT_URL = f'https://{DOMAIN_NAME}/{STAGE}' try: # DynamoDBに存在するコネクションIDを全て取得 items = connection_table.scan(ProjectionExpression='connectionId').get('Items') except Exception as e: logger.error(e) selected_image = json.loads(event.get('body', '{}')).get('selectedImage') apigw_management = boto3.client('apigatewaymanagementapi', endpoint_url=WEBSOCKET_ENDPOINT_URL) for item in items: try: # 画像保存先のS3 URLを構成して、コネクションごとにデータとしてそのURLを送る image_s3 = f'{S3_ENDPOINT_URL}/{selected_image}.png' apigw_management.post_to_connection(ConnectionId=item['connectionId'], Data=image_s3) logger.info(f'ConnectionID: {item["connectionId"]}, image: {image_s3}') except Exception as e: logger.error(e) return {'statusCode': 500, 'body': e} return {'statusCode': 200, 'body': 'Data sent.'} |
デプロイ(AWS SAM)
先ほどの構成図のS3以外はAWS SAMで構築できるように準備しています。template.yamlはAWS公式のサンプルを参考にしています。画像保存用のS3は別で作成しておきます。lambda_function.pyは上で説明したLambdaの処理をそれぞれ記述して下さい。以下のディレクトリ構成でsam deployを実行すればデプロイされます。
コマンド
1 |
sam deploy --template ./template.yaml --s3-bucket (sam用s3バケット名) --stack-name (CFnスタック名) |
ディレクトリ構成
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 |
/ ├── src/ │ │ │ ├──── onconnect/ │ │ │ │ │ └─── lambda_function.py │ │ │ ├──── disconnect/ │ │ │ │ │ └─── lambda_function.py │ │ │ └──── sendimage/ │ │ │ └─── lambda_function.py │ └── template.yaml |
template.yaml のソースコード
https://github.com/mrtmyix/image-change-websocket-test/blob/main/template.yaml
クライアント
以下のような簡易な画像表示のHTMLを用意しました。ラジオボタンで選択して送信すると、選択した値を$sendimageに送ります。すると画像は選択したものに表示が変わって、同じく接続中セッションでも同じ画像が表示されるはずです。
HTMLファイルは以下です。WebSocket URLをSAMデプロイして割り当てられたURLに変更が必要です。
クライアント側のHTMLソースコード
https://github.com/mrtmyix/image-change-websocket-test/blob/main/index.html
WebSocket URLはAPI Gatewayのコントロールパネルから確認ができます。
onopen()は接続が開かれた時に発火するメソッドで、画面上の文字列「未接続」を「接続中」に変更します。onmessage()は先ほど説明した、$sendimageのLambdaでS3 URLが送られてきた時に発火するメソッドで、現在の表示画像を送られてきたS3 URLの画像に変更します。
動作確認
上で説明したクライアントのHTMLファイルをローカルにダウンロードして、ブラウザで開けば動作を確認することができます。Webサーバなどに配置する必要がないため非常に楽です。
以下は4つのウィンドウで動作させた時の動画です。
おわりに
簡単なモノでも作ってみると勉強になりますね。WebSocketを使えばもっと色々な事ができそうだなと思いました。より深く仕組みを知りたければ、AWS公式サイトだったり、多くの方がブログ等で解説しているので見てみてください。