共用方式為


使用 Socket.IO 建置即時程序碼串流應用程式,並將其裝載於 Azure

Microsoft Word 建置共同建立功能這類即時體驗,可能頗為棘手。

Socket.IO 透過其易於使用的 API,已證明自己是用戶端與伺服器之間即時通訊的程式庫。 不過,Socket.IO 使用者經常回報難以調整 Socket.IO 連線。 有了 Web PubSub for Socket.IO,開發人員不用再為管理持續性連線煩心。

重要

原始 連接字串 只會針對示範目的出現在本文中。

連接字串包含應用程式存取 Azure Web PubSub 服務所需的授權資訊。 連接字串內的存取金鑰類似於服務的根密碼。 在生產環境中,請一律保護您的存取金鑰。 使用 Azure 金鑰保存庫,安全地管理和輪替密鑰,並使用保護連線WebPubSubServiceClient

避免將存取金鑰散發給其他使用者、寫入程式碼,或將其以純文字儲存在他人可以存取的位置。 如果您認為金鑰可能已遭盜用,請輪替金鑰。

概觀

本文說明如何建置可讓程式撰寫員將程式碼撰寫活動串流給受眾的應用程式。 您可以使用下列項目建置這個應用程式:

  • Monaco Editor,支援 Visual Studio Code 的程式碼編輯器。
  • Express,Node.js Web 架構。
  • Socket.IO 程式庫提供的 API,可進行即時通訊。
  • 使用 Web PubSub for Socket.IO 的主機 Socket.IO 連線。

完成的應用程式

完成的應用程式可讓程式碼編輯器使用者分享 Web 連結,藉此讓他人觀看輸入的過程。

已完成程式代碼串流應用程式的螢幕快照。

為了讓程序集中,並且能在 15 分鐘左右理解完畢,本文會定義兩個使用者角色,以及他們在編輯器中可以執行的動作:

  • 撰寫者,可在線上編輯器輸入,並且串流內容
  • 檢視人員,接收撰寫者輸入的即時內容,但無法編輯內容

架構

項目 目的 福利
Socket.IO 程式庫 在後端應用程式和用戶端之間提供低延遲、雙向的資料交換機制 涵蓋大多數即時通訊案例的易用 API
Web PubSub for Socket.IO 使用 Socket.IO 用戶端裝載 WebSocket 或輪詢型持續性連線 支援 100,000 個同時連線;簡化的應用程式架構

此圖顯示 web PubSub for Socket.IO 服務如何連接客戶端與伺服器。

必要條件

若要遵循本文的所有步驟,您需要:

建立 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 並定義選項:

    原始 連接字串 只會針對示範目的出現在本文中。 在生產環境中,請一律保護您的存取金鑰。 使用 Azure 金鑰保存庫,安全地管理和輪替密鑰,並使用保護連線WebPubSubServiceClient

    /*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 伺服器常見的方式稍有不同,如本 Socket.IO 文件所述。 透過這兩個步驟,伺服器端程式碼可以卸載管理 Azure 服務的持續連線。 在 Azure 服務的協助之下,應用程式伺服器擔任輕量型 HTTP 伺服器的角色。

實作商務邏輯

既然您已建立由 Web PubSub 裝載的 Socket.IO 伺服器,現在您可以定義客戶端和伺服器如何使用 Socket.IO 的 API 通訊。 這個流程稱為實作商務邏輯。

  1. 用戶端連線之後,應用程式伺服器會傳送名為 login 的自訂事件,通知用戶端它已登入。

    /*server.js*/
    io.on('connection', socket => {
        socket.emit("login");
    });
    
  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 事件,通知用戶端成功的結果。 用戶端收到具有 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'});
        }
        // ... 
    });
    
  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 的持續連線

撰寫者用戶端

先前所述,您在用戶端有兩個使用者角色:撰寫者和檢視人員。 凡是撰寫者輸入的內容,都會串流至檢視人員的畫面。

  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。用顯示於第一個網頁上的 URL 開啟另一個索引標籤。

如果您在第一個索引標籤撰寫程式碼,應該會看到輸入的內容即時反映在另一個索引標籤。Web PubSub for Socket.IO 可協助訊息在雲端傳遞。 express 伺服器只會提供靜態 index.html 檔案和 /negotiate 端點。