Socket.IO を使用してリアルタイムのコード ストリーミング アプリを構築し、Azure でホストする
Microsoft Word での共同作成機能のようなリアルタイムのエクスペリエンスの構築は、困難な場合があります。
Socket.IO は、その使いやすい API により、クライアントとサーバー間のリアルタイム通信のためのライブラリとして実証されています。 しかし、Socket.IO ユーザーは、Socket.IO の接続のスケーリングに関する問題を報告することがよくあります。 Web PubSub for Socket.IO を使用すれば、開発者が永続的な接続の管理について心配する必要はなくなります。
重要
この記事では、デモ目的でのみ生の接続文字列が表示されます。
接続文字列には、アプリケーションが Azure Web PubSub サービスにアクセスするために必要な認可情報が含まれています。 接続文字列内のアクセス キーは、サービスのルート パスワードに似ています。 運用環境では、常にアクセス キーを保護してください。 Azure Key Vault を使ってキーの管理とローテーションを安全に行い、WebPubSubServiceClient
を使って接続を保護します。
アクセス キーを他のユーザーに配布したり、ハードコーディングしたり、他のユーザーがアクセスできるプレーンテキストで保存したりしないでください。 キーが侵害された可能性があると思われる場合は、キーをローテーションしてください。
概要
この記事では、コーダーが対象ユーザーにコーディング アクティビティをストリーム配信できるようにするアプリを構築する方法について説明します。 このアプリケーションは、次を使用してビルドします。
- Monaco Editor (Visual Studio Code の動力となるコード エディター)。
- Express (Node.js Web フレームワーク)。
- リアルタイム通信向けに Socket.IO ライブラリで提供される API。
- Web PubSub for Socket.IO を使用するホスト Socket.IO 接続。
完成したアプリ
完成したアプリを使用すると、コード エディターのユーザーは、他のユーザーが入力を視聴することができる Web リンクを共有できます。
この記事では、手順を 15 分ほどで集中して消化できるようにするために、次の 2 つのユーザー ロールと、それらがエディターで実行できる操作を定義します。
- ライター (オンライン エディターで入力を行うことができ、コンテンツがストリーム配信される)
- ビューアー (ライターが入力したリアルタイムのコンテンツを受信し、コンテンツを編集することはできない)
アーキテクチャ
項目 | 目的 | メリット |
---|---|---|
Socket.IO ライブラリ | バックエンド アプリケーションとクライアント間の低遅延かつ双方向のデータ交換メカニズムを提供する | ほとんどのリアルタイム通信シナリオをカバーする使いやすい API |
Web PubSub for Socket.IO | Socket.IO クライアントとの WebSocket またはポーリング ベースの永続的な接続のホスティング | 100,000 のコンカレント接続のサポート、シンプルなアプリケーション アーキテクチャ |
前提条件
この記事のすべてのステップを実行するには、以下が必要です。
- Azure アカウント。 Azure サブスクリプションをお持ちでない場合は、開始する前に Azure 無料アカウントを作成してください。
- Azure リソース を管理するための Azure CLI (バージョン 2.29.0 以降) または Azure Cloud Shell。
- Socket.IO の API に関する基本的な知識。
Web PubSub for Socket.IO リソースの作成
Azure CLI を使用してリソースを作成します。
az webpubsub create -n <resource-name> \
-l <resource-location> \
-g <resource-group> \
--kind SocketIO \
--sku Free_F1
接続文字列を取得する
接続文字列を使用すると、Web PubSub for Socket.IO に接続できます。
次のコマンドを実行します。 この記事の続きでアプリケーションを実行する際に必要になるため、返された接続文字列はどこかに保持してください。
az webpubsub key show -n <resource-name> \
-g <resource-group> \
--query primaryKey \
-o tsv
アプリケーションのサーバー側のコードを記述する
サーバー側で作業して、アプリケーションのコードの記述を開始します。
HTTP サーバーの構築
Node.js プロジェクトを作成します。
mkdir codestream cd codestream npm init
サーバー SDK と Express をインストールします。
npm install @azure/web-pubsub-socket.io npm install express
必要なパッケージをインポートして、静的ファイルを提供する HTTP サーバーを作成します。
/*server.js*/ // Import required packages const express = require('express'); const path = require('path'); // Create an HTTP server based on Express const app = express(); const server = require('http').createServer(app); app.use(express.static(path.join(__dirname, 'public')));
/negotiate
というエンドポイントを定義します。 ライター クライアントが最初にこのエンドポイントにヒットします。 このエンドポイントは HTTP 応答を返します。 この応答には、クライアントで永続的な接続を確立するために使用する必要があるエンドポイントが含まれています。 また、クライアントが割り当てられているroom
値も返します。/*server.js*/ app.get('/negotiate', async (req, res) => { res.json({ url: endpoint room_id: Math.random().toString(36).slice(2, 7), }); }); // Make the Socket.IO server listen on port 3000 io.httpServer.listen(3000, () => { console.log('Visit http://localhost:%d', 3000); });
Web PubSub for Socket.IO サーバーの作成
Web PubSub for Socket.IO SDK をインポートして、オプションを定義します。
この記事では、デモ目的でのみ生の接続文字列が表示されます。 運用環境では、常にアクセス キーを保護してください。 Azure Key Vault を使ってキーの管理とローテーションを安全に行い、
WebPubSubServiceClient
を使って接続を保護します。/*server.js*/ const { useAzureSocketIO } = require("@azure/web-pubsub-socket.io"); const wpsOptions = { hub: "codestream", connectionString: process.argv[2] }
Web PubSub for Socket.IO サーバーを作成します。
/*server.js*/ const io = require("socket.io")(); useAzureSocketIO(io, wpsOptions);
この Socket.IO ドキュメントで説明されているように、この 2 つのステップは、通常の Socket.IO サーバーの作成方法とは少し異なります。 この 2 つのステップでは、サーバー側のコードで永続的な接続の管理を Azure サービスにオフロードできます。 Azure サービスを活用すると、アプリケーション サーバーは、軽量 HTTP サーバーとしてのみ動作します。
ビジネス ロジックの実装
Web PubSub でホストされる Socket.IO サーバーが作成されたため、Socket.IO の API を使用してクライアントとサーバーの通信方法を定義できます。 このプロセスをビジネス ロジックの実装と呼びます。
クライアントが接続された後、アプリケーション サーバーでは
login
という名前のカスタム イベントを送信し、ログインしていることをクライアントに通知します。/*server.js*/ io.on('connection', socket => { socket.emit("login"); });
各クライアントは、サーバーで応答できる 2 つのイベント (
joinRoom
とsendToRoom
) を出力します。 クライアントが参加するroom_id
値をサーバーで取得した後、Socket.IO の API からsocket.join
を使用して、指定されたルームにターゲット クライアントを参加させます。/*server.js*/ socket.on('joinRoom', async (message) => { const room_id = message["room_id"]; await socket.join(room_id); });
クライアントが参加した後、サーバーで
message
イベントを送信して、成功した結果をクライアントに通知します。 クライアントで型がackJoinRoom
のmessage
イベントを受信すると、クライアントでサーバーに最新のエディターの状態を送信するように要求できます。/*server.js*/ socket.on('joinRoom', async (message) => { // ... socket.emit("message", { type: "ackJoinRoom", success: true }) });
/*client.js*/ socket.on("message", (message) => { let data = message; if (data.type === 'ackJoinRoom' && data.success) { sendToRoom(socket, `${room_id}-control`, { data: 'sync'}); } // ... });
クライアントで
sendToRoom
イベントをサーバーに送信すると、サーバーでは、 コード エディターの状態に対する変更を指定されたルームにブロードキャストします。 ルーム内のすべてのクライアントで最新情報を受信することが可能になります。socket.on('sendToRoom', (message) => { const room_id = message["room_id"] const data = message["data"] socket.broadcast.to(room_id).emit("message", { type: "editorMessage", data: data }); });
アプリケーションのクライアント側のコードを記述する
サーバー側の手順が完了したため、クライアント側での作業が可能になります。
初期セットアップ
サーバーと通信するには、Socket.IO クライアントを作成する必要があります。 問題は、クライアントでどのサーバーと永続的な接続を確立する必要があるのかです。 Web PubSub for Socket.IO を使用しているため、当該のサーバーは Azure サービスです。 /negotiate ルートを定義して、クライアントに Web PubSub for Socket.IO へのエンドポイントを提供したことを思い出しましょう。
/*client.js*/
async function initialize(url) {
let data = await fetch(url).json()
updateStreamId(data.room_id);
let editor = createEditor(...); // Create an editor component
var socket = io(data.url, {
path: "/clients/socketio/hubs/codestream",
});
return [socket, editor, data.room_id];
}
initialize(url)
関数では、次のいくつかのセットアップ操作を編成します。
- HTTP サーバーから Azure サービスにエンドポイントをフェッチする
- Monaco Editor インスタンスを作成する
- Web PubSub for Socket.IO との永続的な接続を確立する
ライター クライアント
先程述べたように、クライアント側にはライターとビューアーの 2 つのユーザー ロールがあります。 ライターが入力するものはすべて、ビューアーの画面にストリーム配信されます。
Web PubSub for Socket.IO へのエンドポイントと
room_id
値を取得します。/*client.js*/ let [socket, editor, room_id] = await initialize('/negotiate');
ライター クライアントをサーバーに接続すると、サーバーではライターに
login
イベントを送信します。 ライターでは、指定されたルームへの参加をサーバーに要求すると、応答できます。 ライター クライアントでは、200 ミリ秒ごとに、最新のエディターの状態をルームに送信します。flush
という名前の関数では、送信ロジックを編成します。/*client.js*/ socket.on("login", () => { updateStatus('Connected'); joinRoom(socket, `${room_id}`); setInterval(() => flush(), 200); // Update editor content // ... });
ライターが編集を行わない場合、
flush()
は何も実行せず、単に戻ります。 それ以外の場合は、エディターの状態に対する変更がルームに送信されます。/*client.js*/ function flush() { // No changes from editor need to be flushed if (changes.length === 0) return; // Broadcast the changes made to editor content sendToRoom(socket, room_id, { type: 'delta', changes: changes version: version++, }); changes = []; content = editor.getValue(); }
新しいビューアー クライアントが接続されている場合、ビューアーではエディターの最新の完全な状態を取得する必要があります。 これを実現するために、
sync
データを含むメッセージがライター クライアントに送信されます。 このメッセージは、ライター クライアントに対して、エディターの完全な状態を送信するように求めます。/*client.js*/ socket.on("message", (message) => { let data = message.data; if (data.data === 'sync') { // Broadcast the full content of the editor to the room sendToRoom(socket, room_id, { type: 'full', content: content version: version, }); } });
ビューアー クライアント
ライター クライアントと同様に、ビューアー クライアントでは
initialize()
を使用して Socket.IO クライアントを作成します。 ビューアー クライアントでは、接続されてサーバーからlogin
イベントを受信すると、指定されたルームへの参加をサーバーに対して求めます。 クエリroom_id
でルームを指定します。/*client.js*/ let [socket, editor] = await initialize(`/register?room_id=${room_id}`) socket.on("login", () => { updateStatus('Connected'); joinRoom(socket, `${room_id}`); });
ビューアー クライアントでサーバーから
message
イベントを受信し、データ型がackJoinRoom
である場合、ビューアー クライアントでは、ルーム内のライター クライアントに対して、エディターの完全な状態を送信するように要求します。/*client.js*/ socket.on("message", (message) => { let data = message; // Ensures the viewer client is connected if (data.type === 'ackJoinRoom' && data.success) { sendToRoom(socket, `${id}`, { data: 'sync'}); } else //... });
データ型が
editorMessage
の場合、ビューアー クライアントでは、実際のコンテンツに応じてエディターを更新します。/*client.js*/ socket.on("message", (message) => { ... else if (data.type === 'editorMessage') { switch (data.data.type) { case 'delta': // ... Let editor component update its status break; case 'full': // ... Let editor component update its status break; } } });
Socket.IO の API を使用して
joinRoom()
とsendToRoom()
を実装します。/*client.js*/ function joinRoom(socket, room_id) { socket.emit("joinRoom", { room_id: room_id, }); } function sendToRoom(socket, room_id, data) { socket.emit("sendToRoom", { room_id: room_id, data: data }); }
アプリケーションの実行
リポジトリの検出
前のセクションでは、ビューアーとライター間におけるエディターの状態の同期に関連したコア ロジックについて説明しました。 完全なコードは、サンプル リポジトリにあります。
リポジトリを複製する
リポジトリを複製して、npm install
を実行すると、プロジェクトの依存関係をインストールできます。
サーバーを起動する
node server.js <web-pubsub-connection-string>
これは、前のステップで受信した接続文字列です。
リアルタイムのコード エディターを試す
ブラウザー タブで http://localhost:3000
を開きます。最初の Web ページに表示された URL で別のタブを開きます。
最初のタブでコードを記述すると、もう一方のタブにリアルタイムで入力が反映されるのを確認できるはずです。Web PubSub for Socket.IO は、クラウドでのメッセージの受け渡しを支援します。 express
サーバーは、静的な index.html
ファイルと /negotiate
エンドポイントのみを提供します。