なぜウェブソケットは難しいのか?

WebSocket: モダンアプリのクールキッズだが、飼い慣らすのはとても難しい!

数年前、私は自分のウェブ・アプリケーションにリアルタイム通知を導入するプロジェクトに取り組んでいました。リアルタイム “というアイデアに興奮した私は、すぐにWebSocketを実装する機会を得ようと思いました。

WebSocketが何をするものかは知っていたが、それが何なのかはよく知らなかったのです。
つまり、サーバーからブラウザにメッセージを送れることは知っていたが、どうやって送るのかは知らず、バックエンドとの間でデータをプッシュするために使用できる “コネクション “があるという事実以上のことは知らなかったのです。

私は2日間の作業だと思っていたものを作るために出発しました。何がうまくいかないというのだろうか?

その後、私はソフトウェア・エンジニアであることを考え直すような複雑な下降スパイラルに陥いりました。それについて話そうと思います。

WebSocket API構造

私はRESTのバックグラウンドを持っています。エンドポイントにはリソースベースのパスがあり、どのhttpメソッド(GET = データをロード、POST = データを作成、PUT = データを更新、など)を使っているかによってインテントが示されます。

AWS API Gatewayのドキュメントで最初に目にしたのは、奇妙な$connect$disconnectルートだった。命名規則から、これらのルートが何をするものなのか想定はしていたが、それをどうすればいいのかわからなかった。

接続しようとしているユーザーを一意に識別する方法は、私には直感的ではありませんでした。一度接続が確立されれば、この接続を越えてデータが自由に行き来するのかどうかもわかりませんでした。また、接続をどのように追跡すればいいのか、追跡する必要があるのかどうかもわからず、次から次へとウサギの穴が現れるだけだった。

結局、AWS API Gatewayでは、接続はサービス自体によって管理されるが、誰が接続され、どのような情報を受け取ったかを追跡するのはあなた(開発者)の責任であることがわかりました。また、データはただ自由に行き来できるわけではないことも知りました。

クライアントからサーバーへのインタラクションでは、独自のルートを定義し、バックエンド・コンピューティング・リソースを指定する必要があります。各ルートは、API Gateway V2 Route、API Gateway V2 Integration、Lambda関数、Lambda関数のパーミッションリソースをInfrastructure as Codeで定義する必要があり、1ルートあたり約50行でした。

サーバーからクライアントに送るデータについては、何でも送ることができます。異なるタイプのメッセージを識別するための規約を作成し、それらを適切に扱えるようにする必要があります。

クライアント対サーバーとサーバー対クライアントの違いに、私は戸惑いました。一方は非常に硬直的で構造化されており、もう一方はルーズなのです。スケーラブルでメンテナンス可能なソフトウェアを構築する方法とはとても思えませんでした。

コネクション管理

先に述べたように、API Gatewayはあなたに代わってコネクションの管理を行うが、どのコネクションにどのデータを送信するかはあなたの責任です。例を見てみましょう:

当社のユーザーMalloryがTaylor Swift、Adele、Ed Sheeranのチケットが入手可能になったら通知を受けたいとします。彼女がチケット販売サイトに接続すると、データベースに4つのレコードが保存されます:

・接続とユーザー・メタデータを識別する1つのレコード
・通知を受けたいアーティストごとに1レコード

アーティストレコードの場合、pkは彼女の接続IDで、skは購読レコードであることを示します。Ed Sheeranのチケットが発売されたことを示すイベントを取得すると、彼にサブスクライブしているすべての接続に即座に通知できるように、アーティスト名をGSIとして追加します。

AWSのサーバーレス・バックエンドでサブスクライバーに通知するために、どのアーティストのチケットが入手可能かをEventBridgeイベントでLambda関数をトリガーする。この関数は、DynamoDBのアーティストGSIにクエリし、そのアーティストにサブスクライブしている全ての接続を見つけ、そして、各レコードを繰り返し、チケット情報を接続ユーザーに公開します。これは大変な作業です!

ユーザーが切断すると、接続IDを含むpkを持つすべてのレコードをデータベースに照会し、削除することができます。API Gatewayからの切断イベントを見逃した場合、接続レコードに24時間(またはユースケースに合った時間)の生存時間(TTL)を設定し、自動的に削除します。

これは、「技術的には」ビジネス価値のないもののための多くのインフラです。これは単にユーザーにアラートを出すマイクロサービスです。これは、陳腐化したり、遅くなったり、非推奨になったりする可能性のあるコードを、時間をかけてメンテナンスしなければならないものです。結局のところ、コードは負債になります。

Security

私はGovTech出身です。アプリは過度にセキュアでなければセキュアとはいえません。だから、WebSocket APIでauthをサポートする唯一のルートが$connectだと知ったときは、少し驚きました。一旦接続が確立されれば、authヘッダーやその他の認証情報を渡すことなく、好きなルートを自由に呼び出すことができます。

これについてはしばらく考えてみたが、理論的には理にかなっています。WebSocket接続はステートフルなので、電話をかけるたびに再認証する必要はないはずです。それは、誰かの家のドアをノックして自分の名前を言い、中に入ってから何かをするたびに自分が誰であるかを言い直すようなものです。それでは意味がありません。

WebSocketにauthヘッダーを渡すのも、思ったほど簡単ではありません。SocketIOのような一般的なクライアントは、クライアントとサーバーの両方で使用しない限り、authヘッダーをうまくサポートしていません。AWSでホストされているWebSocketにベアラートークンを渡すために私が見つけた最良の方法は、クエリストリングパラメータを使用することでした。Sec-WebSocket-Protocolヘッダを再利用して、サブプロトコルと認証トークンの両方を受け取ることもできるが、これは一般的な方法に反します。

Client-side SDKs

人々はSocketIOが大好きなようだ。npmで毎週400万以上のダウンロードがあり、間違いなくWebSocketに接続する優れた方法のひとつです。しかし、人気があるからといって簡単というわけではありません。

なぜかAPI Gatewayで動作させるのに大苦戦しました。WebSocketプロトコル(httpsの代わりにwss)とAWSがAPIをセットアップする方法がうまくいかなかったのです。

試行錯誤を繰り返し、認証をずらしたり、怒りでやめたりしながら、一度か二度、ユーザー・インターフェースにWebSocketを接続することができました。しかし、そのたびに、うまくやるためのコツを学び直さなければなりません。SocketIOのように何でもできるようになると、直感性や開発者の経験が少し失われることがあるのです。

より簡単な方法

Momento Topicsでは、WebSocketの難しい部分はすべて抽象化されていいます。API構造を構築する必要はありません。購読者は、1回のAPIコールで特定のチャンネルに接続し、更新を登録できます:


await topicClient.subscribe('websocket', 'mychannel', {
    onItem: (data) => { handleItem(data.valueString()); },
    onError: (err) => { console.error(err); }
});

チャンネルにパブリッシュする場合、呼び出しはさらにシンプルになります:

await topicClient.publish('websocket', 'mychannel', JSON.stringify({ detail }));

サービスとサービス、サービスとブラウザ、ブラウザとブラウザをTopicsで接続することができます。サービスは接続管理にMomentoのサーバーを使用するため、サーバーを介さずに2つのブラウザセッションを通信させるなど、これまで不可能だったオプションが利用できます。このため、データが準備できたときに公開することと、アップデートを購読することの2つの責任が残ります。

他のサーバーレスサービスと同様、Momento Topicsはセキュリティを最優先しているが、アクセスを制限する柔軟なオプションも用意されています。きめ細かなアクセス制御により、API トークンのスコープを可能な限り狭く設定できます。アクセスポリシーの例を以下に示します:

const tokenScope = {
    permissions: [
        {
            role: 'subscribeonly',
            cache: 'websocket',
            topic: 'mychannel'
        }
    ]
};

この権限セットで作成された API トークンは、ウェブ・ソケット・キャッシュ内の mychannel トピックへのサブスクライブしか許可されません。もし誰かがこのトークンを横取りして、データを公開しようとしたり、別のトピックを購読しようとしたりすると、認証エラーが発生します。

Momentoには、統合するためのSDKが多数用意されています。ブラウザでは、Web SDKを使用できます。サーバーサイドの開発では、TypeScript/JavascriptPythonGoに対応したTopicsサービスが利用可能で、.NETJavaElixirPHPRubyRustも近日中にサポートされる予定です。

何が問題なのか?

うまくいけば、それはあまりにも良いことに聞こえるでしょう。私も最初はそうでした。今でもそうです。しかし問題はありません。Momentoの使命は、サーバーレス・サービスでクラス最高の開発者体験を提供し、開発者の負担をできる限り軽減することです。

複雑な接続管理やイベント・ルーティングを処理する通知サービスの構築に何週間も費やす必要はありません。MomentoのようなSaaSプロバイダーに運用のオーバーヘッドを任せることで、本当に重要なことに集中することができます。

料金設定はシンプルで、データ転送量は$.50/GBで、5GBの永久無料Tierが設定されています。試さない理由はありません!

例をお探しですか?Next.jsのTopicsで構築された高機能なチャットアプリケーションをご覧ください。また、Momento CacheとTopicsの両方で構築された、進行中のゲーム「Acorn Hunt」もお試しください。

ご質問、フィードバック、機能リクエストなどがありましたら、Discordまたはウェブサイトを通じてチームにお知らせください。これらのサービスは私たち全員のためのものであり、安全かつ迅速に本番を迎えられるよう、最高の製品を作りたいと思っています。

Happy coding!‍