【Flutter】 AWS APIGatewayのWebSocket APIを使ってみた

FlutterでAWS API GatewayのWebSocket APIを使ってみたい！という方向けの記事になります。
本記事は実装方法に特化して書いていますので、そもそもWebSoketって？という方は参考記事をご参照ください。
# 前提
- Flutterの環境構築は済んでおり、基礎は知っている
- AWSアカウント作成済み
# 構成
```mermaid
graph LR
    Flutter-->APIGateway
    APIGateway --接続時--> Lambda
    APIGateway --切断時--> Lambda
		APIGateway-- カスタム -->Lambda
```
# 手順

大まかな手順としては↓になります。

1. Lambdaに関数を作成
2. APIGatewayでAPIを作成
3. Flutter側の実装

# Lambdaに関数を作成
## 関数の作成
Lambdaのコンソールを開き「関数の作成」を選択します。
![関数.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/643790/c0a2baec-d610-6f2a-944f-0945d06ded25.png)
各種設定を選択。
「一から作成」を選択。
| 項目名 | 設定内容 |
| --- | --- |
| 関数名 | 好きな関数名を入力 |
| ランタイム | Node.js 16.x |
| アーキテクチャ | x86_64 |

（他の項目は特に設定しなくても問題ありません）
![関数の作成.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/643790/9b7c27da-e86d-cfb6-d774-bf28e95cad32.png)
「関数の作成」を押すと、関数が作成されます。

## 各種処理追加
作成した関数を開き、ディレクトリ構成を以下のようにします。
【】→フォルダ名
- 【関数名】
    - 【interface】
        - connect.js
        - disconnect.js
        - sendText.js
    - index.js

### index.js

```jsx: index.js
const { connect } = require('./interface/connect');
const { sendText } = require('./interface/sendText');
const { disconnect } = require('./interface/disconnect'); 

exports.handler = async (event, context) => {
	// レスポンスデータ
    let response;
    // イベントタイプ         
    let routeKey;          
        
    //-------------------------------------
    //-- eventTypeからIF毎のロジックに振り分け
    //-------------------------------------
    routeKey = event.requestContext.routeKey;

    switch (routeKey){
        
        case "$connect":
            //***************************************************
            //** 接続
            //***************************************************
            response = await connect(event, context);
            break;
            
        case "sendText":
            //***************************************************
            //** 文字送信
            //***************************************************
            response = await sendText(event, context);
            break;
            
        case "$disconnect":
            //***************************************************
            //** 切断
            //***************************************************
            response = await disconnect(event, context);
            break;
    }

    return response_final;

};
```

### connect.js

```jsx:connect.js
exports.connect = async (event, context, callback) => {
    // ***************************************************
    // ** 接続時の処理を好きに追加
    // ***************************************************
    
    // レスポンスデータの生成
    let response = {
        "statusCode": event.statusCode,
        "headers": event.headers,
        "body": context.body,
        "isBase64Encoded": false
    };
    return response;
};
```

### disconnect.js

```jsx:disconnect.js
exports.disconnect = async (event, context, callback) => {
    // ***************************************************
    // ** 切断時の処理を好きに追加
    // ***************************************************
    
    // レスポンスデータの生成
    let response = {
        "statusCode": event.statusCode,
        "headers": event.headers,
        "body": context.body,
        "isBase64Encoded": false
    };
    return response;
};
```

### sendText.js
`sendText`が呼ばれた時に「送信されてきたdataを、送信者にそのまま送信する」ようにしています。
```jsx:sendText.js
const { ApiGatewayManagementApiClient, PostToConnectionCommand } = require("@aws-sdk/client-apigatewaymanagementapi");

exports.sendText = async (event, context, callback) => {
    // ***************************************************
    // ** 実装例）sendText実行時の処理
    // ***************************************************
    // apigateway client インスタンス取得
	const domain = event.requestContext.domainName;
    const stage = event.requestContext.stage;
    const endpoint = `https://${domain}/${stage}`;
    const agmClient = new ApiGatewayManagementApiClient({ endpoint: endpoint });
    
    //----------------------------------------------
    //-- clientにdata送信
    //----------------------------------------------
    // clientに送信するデータ
	const body = JSON.parse(event.body);    
	const sendData = {
        text : body.data.text
    };
    
    const sendParams = {
        Data: Buffer.from(JSON.stringify(sendData))
    };
	// 送信先のconnectionId
    sendParams.ConnectionId = event.requestContext.connectionId;
    
    try {
      // クライアントに送信
      response = await agmClient.send( new PostToConnectionCommand(sendParams));
      // 送信成功時の処理
				      
    } catch (error) {
      // 送信失敗時の処理        
    }
    
    // レスポンスデータの生成
    response = {
        "statusCode": event.statusCode,
        "headers": event.headers,
        "body": context.body,
        "isBase64Encoded": false
    };
    return response;
};
```

## Deploy 
「Deploy」ボタンを押します。
これやらないと反映されないので、お忘れなく。
![デプロイ.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/643790/b6b19397-9f12-95e0-60a1-be176c1a3820.png)

## 実行ロールの追加
LambdaからAPIGatewayにアクセスできるように、実行ロールを追加します。
![ロール.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/643790/5ad3a2ca-feb8-9a31-ddf1-bdcbd2be6244.png)
編集内容は以下の公式ドキュメントを参照
https://docs.aws.amazon.com/ja_jp/apigateway/latest/developerguide/apigateway-websocket-control-access-iam.html

# APIGatewayでAPIを作成
## APIの作成
AWS API Gatewayのコンソールに行き「APIの作成」を選択します。
![APIを作成.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/643790/a49e203d-9d73-bd11-af6e-a19c7551f903.png)
今回作成するのはWebSocket APIなので「WebSocket API」の「構築」を押します。
![APIタイプを選択.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/643790/92b47aa6-83b2-7ce7-7e81-0f574db4542b.png)
## APIの詳細を指定する
| 項目名 | 設定内容 |
| --- | --- |
| API名 | 好きなAPI名を入力 |
| ルート選択式 | 「request.body.action」を入力 |

![詳細を指定.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/643790/b394ca8c-0a89-8544-9640-8e5d43f3510f.png)
入力したら「次へ」ボタンを押します。

## ルートを追加
1. `$connectルートの追加` `$disconnectルートの追加` `$defaultルートの追加`を押下。
2. 「カスタムルートを追加」を押し、入力欄に「sendText」と入力します。
3. 「次へ」ボタンを押下。
![ルートの追加.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/643790/7e107d1d-2152-1ba2-e440-0afd207fd71f.png)

## 統合をアタッチする
すべてのルートに対して同じLambda関数を設定します。
どのルートから呼び出されているかはLambda側で判定するため、同じもので問題ありません。
| 項目名 | 設定内容 |
| --- | --- |
| 統合タイプ | Lambda |
| AWS リージョン | 使用しているリージョン |
| Lambda関数 | 呼び出す関数（先ほど作成したもの） |

![統合のアタッチ.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/643790/9b5e8982-de79-18b7-9d10-1b21532a7a28.png)
![統合タイプ.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/643790/d8f31b78-d66a-ad92-6713-0e86df464820.png)

## ステージの追加
![ステージの追加.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/643790/603a407f-8d68-e327-ae81-655e45869801.png)

| 項目名 | 設定内容 |
| --- | --- |
| ステージ名 | 好きなステージ名を入力 |

## 確認して作成
1. 設定内容を確認
2. 「作成してデプロイ」を押下
![確認して作成.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/643790/c08c8ccd-270c-af91-5f62-006ab476450d.png)

# Flutter側の実装

## パッケージの追加

https://pub.dev/packages/web_socket_channel

```dart
dependencies:
  flutter:
    sdk: flutter

  # WebSoket
  web_socket_channel: ^2.2.0
```

## 接続

接続用のインスタンスの作成

```dart
// WebSocketインスタンス
WebSocketChannel? _channel;
```

使用したい画面のライフサイクルメソッドなどで、以下のメソッドを呼びます。

```dart
// WebSocket接続開始
void startConnection(){
	_channel = IOWebSocketChannel.connect("wss://xxxxxxxxx");
	
	// 切断時の監視
	final subscription = _channel?.stream.listen(null);
	subscription?.onDone(() {
	  // WebSocket切断時の処理
	});
}
```

接続先のURLはAPIGatewayで以下の画面の「WebSocket URL」を使用します。
wssから始まるやつです。
![URL.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/643790/68bca775-bc7b-a373-366b-c9b817435d79.png)

サーバーを監視し、変更があれば取得・表示できるようにします。
ここでは受け取ったdataをTextに変換して表示しておきます。

```dart
StreamBuilder(
	stream: _channel?.stream,
	builder: (context, snapshot) {
	  return Text(snapshot.hasData
	      ? 'response:${snapshot.data}'
	      : 'response:');
},
```

## 切断
切断する時は以下のメソッドを呼ぶだけです。
```dart
void disconnect() async {
  await _channel?.sink.close();
}
```

## 送信
APIGateway WebSocket APIの場合は、ただデータを送るだけでなく、ルートを指定する必要があります。
以下のfunctionを、ボタン押下時などに呼び出してください。
```dart
// 文字送信
void sendTextAction() {   
  Map data = {
	// 送信したいAPIのルート名を指定
    "action": "sendText",
	// 送信するデータ    
	"data":
    {
      "text": "文字送ったよ",
    } 
  };
  // 送信処理
  _channel?.sink.add(jsonEncode(data));      
}
```
# まとめ
今回苦戦したのがAWS周りだったので、Flutterはだいぶ省略してしまいました。お許しください。
どなたかの一助となれば幸いです。

# 参考記事

https://docs.aws.amazon.com/ja_jp/apigateway/latest/developerguide/apigateway-websocket-api.html

https://kimuralog.com/?p=2444

https://qiita.com/chihiro/items/9d280704c6eff8603389

https://qiita.com/minsu/items/fcae56cd175d1d584709

https://qiita.com/G-awa/items/472bc1a9d46178f3d7a4

https://zenn.dev/mryhryki/articles/2020-12-01-aws-api-gateway-websocket