Share via


Socket.IO を使用してリアルタイムのコード ストリーミング アプリを構築し、Azure でホストする

Microsoft Word での共同作成機能のようなリアルタイムのエクスペリエンスの構築は、困難な場合があります。

Socket.IO は、その使いやすい API により、クライアントとサーバー間のリアルタイム通信のためのライブラリとして実証されています。 しかし、Socket.IO ユーザーは、Socket.IO の接続のスケーリングに関する問題を報告することがよくあります。 Web PubSub for Socket.IO を使用すれば、開発者が永続的な接続の管理について心配する必要はなくなります。

概要

この記事では、コーダーが対象ユーザーにコーディング アクティビティをストリーム配信できるようにするアプリを構築する方法について説明します。 このアプリケーションは、次を使用してビルドします。

  • Monaco Editor (Visual Studio Code の動力となるコード エディター)。
  • Express (Node.js Web フレームワーク)。
  • リアルタイム通信向けに Socket.IO ライブラリで提供される API。
  • Web PubSub for Socket.IO を使用するホスト Socket.IO 接続。

完成したアプリ

完成したアプリを使用すると、コード エディターのユーザーは、他のユーザーが入力を視聴することができる Web リンクを共有できます。

Screenshot of the finished code-streaming app.

この記事では、手順を 15 分ほどで集中して消化できるようにするために、次の 2 つのユーザー ロールと、それらがエディターで実行できる操作を定義します。

  • ライター (オンライン エディターで入力を行うことができ、コンテンツがストリーム配信される)
  • ビューアー (ライターが入力したリアルタイムのコンテンツを受信し、コンテンツを編集することはできない)

アーキテクチャ

項目 目的 メリット
Socket.IO ライブラリ バックエンド アプリケーションとクライアント間の低遅延かつ双方向のデータ交換メカニズムを提供する ほとんどのリアルタイム通信シナリオをカバーする使いやすい API
Web PubSub for Socket.IO Socket.IO クライアントとの WebSocket またはポーリング ベースの永続的な接続のホスティング 100,000 のコンカレント接続のサポート、シンプルなアプリケーション アーキテクチャ

Diagram that shows how the Web PubSub for Socket.IO service connects clients with a server.

前提条件

この記事のすべてのステップを実行するには、以下が必要です。

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 サーバーの構築

  1. Node.js プロジェクトを作成します。

    mkdir codestream
    cd codestream
    npm init
    
  2. サーバー SDK と Express をインストールします。

    npm install @azure/web-pubsub-socket.io
    npm install express
    
  3. 必要なパッケージをインポートして、静的ファイルを提供する 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')));
    
  4. /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 サーバーの作成

  1. Web PubSub for Socket.IO SDK をインポートして、オプションを定義します。

    /*server.js*/
    const { useAzureSocketIO } = require("@azure/web-pubsub-socket.io");
    
    const wpsOptions = {
        hub: "codestream",
        connectionString: process.argv[2]
    }
    
  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 を使用してクライアントとサーバーの通信方法を定義できます。 このプロセスをビジネス ロジックの実装と呼びます。

  1. クライアントが接続された後、アプリケーション サーバーでは login という名前のカスタム イベントを送信し、ログインしていることをクライアントに通知します。

    /*server.js*/
    io.on('connection', socket => {
        socket.emit("login");
    });
    
  2. 各クライアントは、サーバーで応答できる 2 つのイベント ( joinRoomsendToRoom) を出力します。 クライアントが参加する 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);
    });
    
  3. クライアントが参加した後、サーバーで message イベントを送信して、成功した結果をクライアントに通知します。 クライアントで型が ackJoinRoommessage イベントを受信すると、クライアントでサーバーに最新のエディターの状態を送信するように要求できます。

    /*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'});
        }
        // ... 
    });
    
  4. クライアントで 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 つのユーザー ロールがあります。 ライターが入力するものはすべて、ビューアーの画面にストリーム配信されます。

  1. Web PubSub for Socket.IO へのエンドポイントと room_id 値を取得します。

    /*client.js*/
    
    let [socket, editor, room_id] = await initialize('/negotiate');
    
  2. ライター クライアントをサーバーに接続すると、サーバーではライターに login イベントを送信します。 ライターでは、指定されたルームへの参加をサーバーに要求すると、応答できます。 ライター クライアントでは、200 ミリ秒ごとに、最新のエディターの状態をルームに送信します。 flush という名前の関数では、送信ロジックを編成します。

    /*client.js*/
    
    socket.on("login", () => {
        updateStatus('Connected');
        joinRoom(socket, `${room_id}`);
        setInterval(() => flush(), 200);
        // Update editor content
        // ...
    });
    
  3. ライターが編集を行わない場合、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();
    }
    
  4. 新しいビューアー クライアントが接続されている場合、ビューアーではエディターの最新の完全な状態を取得する必要があります。 これを実現するために、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,
            });
        }
    });
    

ビューアー クライアント

  1. ライター クライアントと同様に、ビューアー クライアントでは 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}`);
    });
    
  2. ビューアー クライアントでサーバーから 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 //...
    });
    
  3. データ型が 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;
            }
        }
    });
    
  4. 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 エンドポイントのみを提供します。