Tworzenie aplikacji przesyłania strumieniowego kodu w czasie rzeczywistym przy użyciu Socket.IO i hostowanie jej na platformie Azure

Tworzenie środowiska w czasie rzeczywistym, takiego jak funkcja współtworzenia w programie Microsoft Word , może być trudne.

Dzięki łatwym w użyciu interfejsom API Socket.IO okazała się biblioteką komunikacji w czasie rzeczywistym między klientami a serwerem. Jednak Socket.IO użytkownicy często zgłaszają trudności ze skalowaniem połączeń Socket.IO. Dzięki usłudze Web PubSub dla Socket.IO deweloperzy nie muszą już martwić się o zarządzanie połączeniami trwałymi.

Omówienie

W tym artykule pokazano, jak utworzyć aplikację, która umożliwia programowi coder przesyłanie strumieniowe działań kodowania do odbiorców. Ta aplikacja jest kompilowany przy użyciu:

  • Edytor Monaco, edytor kodu, który obsługuje program Visual Studio Code.
  • Express, platforma internetowa Node.js.
  • Interfejsy API, które biblioteka Socket.IO zapewnia do komunikacji w czasie rzeczywistym.
  • Hostowanie połączeń Socket.IO korzystających z usługi Web PubSub na potrzeby Socket.IO.

Zakończona aplikacja

Gotowa aplikacja umożliwia użytkownikowi edytora kodu udostępnianie linku internetowego, za pomocą którego użytkownicy mogą obserwować wpisywanie.

Screenshot of the finished code-streaming app.

Aby procedury były skoncentrowane i szyfrowane w ciągu około 15 minut, w tym artykule zdefiniowano dwie role użytkownika i czynności, które mogą wykonywać w edytorze:

  • Składnik zapisywania, który może wpisać w edytorze online i zawartość jest przesyłana strumieniowo
  • Osoby przeglądające, które otrzymują zawartość w czasie rzeczywistym wpisanym przez składnik zapisywania i nie mogą edytować zawartości

Architektura

Produkt Przeznaczenie Świadczenia
biblioteka Socket.IO Zapewnia mechanizm wymiany danych dwukierunkowych o małych opóźnieniach między aplikacją zaplecza a klientami Łatwe w użyciu interfejsy API, które obejmują większość scenariuszy komunikacji w czasie rzeczywistym
Web PubSub for Socket.IO Hostuje połączenia trwałe protokołu WebSocket lub oparte na sondach z klientami Socket.IO Obsługa 100 000 połączeń współbieżnych; uproszczona architektura aplikacji

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

Wymagania wstępne

Aby wykonać wszystkie kroki opisane w tym artykule, potrzebne są następujące elementy:

Tworzenie usługi Web PubSub dla zasobu Socket.IO

Użyj interfejsu wiersza polecenia platformy Azure, aby utworzyć zasób:

az webpubsub create -n <resource-name> \
                    -l <resource-location> \
                    -g <resource-group> \
                    --kind SocketIO \
                    --sku Free_F1

Pobieranie parametry połączenia

Parametry połączenia umożliwia nawiązywanie połączenia z usługą Web PubSub na potrzeby Socket.IO.

Uruchom następujące polecenia. Zachowaj zwróconą parametry połączenia gdzieś, ponieważ będzie ona potrzebna po uruchomieniu aplikacji w dalszej części tego artykułu.

az webpubsub key show -n <resource-name> \ 
                      -g <resource-group> \ 
                      --query primaryKey \
                      -o tsv

Pisanie kodu po stronie serwera aplikacji

Zacznij pisać kod aplikacji, pracując po stronie serwera.

Tworzenie serwera HTTP

  1. Utwórz projekt Node.js:

    mkdir codestream
    cd codestream
    npm init
    
  2. Zainstaluj zestaw SDK serwera i platformę Express:

    npm install @azure/web-pubsub-socket.io
    npm install express
    
  3. Zaimportuj wymagane pakiety i utwórz serwer HTTP do obsługi plików statycznych:

    /*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. Zdefiniuj punkt końcowy o nazwie /negotiate. Klient modułu zapisywania najpierw osiąga ten punkt końcowy. Ten punkt końcowy zwraca odpowiedź HTTP. Odpowiedź zawiera punkt końcowy, którego klient powinien użyć do nawiązania trwałego połączenia. Zwraca również wartość przypisaną room przez klienta.

    /*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);
    });
    

Tworzenie usługi Web PubSub dla serwera Socket.IO

  1. Zaimportuj usługę Web PubSub dla zestawu SDK Socket.IO i zdefiniuj opcje:

    /*server.js*/
    const { useAzureSocketIO } = require("@azure/web-pubsub-socket.io");
    
    const wpsOptions = {
        hub: "codestream",
        connectionString: process.argv[2]
    }
    
  2. Utwórz web pubSub dla serwera Socket.IO:

    /*server.js*/
    
    const io = require("socket.io")();
    useAzureSocketIO(io, wpsOptions);
    

Te dwa kroki różnią się nieco od sposobu tworzenia serwera Socket.IO zgodnie z opisem w tej dokumentacji Socket.IO. W tych dwóch krokach kod po stronie serwera może odciążyć zarządzanie trwałymi połączeniami z usługą platformy Azure. Za pomocą usługi platformy Azure serwer aplikacji działa tylko jako lekki serwer HTTP.

Implementowanie logiki biznesowej

Po utworzeniu serwera Socket.IO hostowanego przez usługę Web PubSub można zdefiniować sposób komunikowania się klientów i serwerów przy użyciu interfejsów API Socket.IO. Ten proces jest nazywany implementowanie logiki biznesowej.

  1. Po nawiązaniu połączenia klient aplikacji informuje klienta, że jest zalogowany, wysyłając zdarzenie niestandardowe o nazwie login.

    /*server.js*/
    io.on('connection', socket => {
        socket.emit("login");
    });
    
  2. Każdy klient emituje dwa zdarzenia, na które serwer może reagować: joinRoom i sendToRoom. Gdy serwer pobierze room_id wartość, którą klient chce dołączyć, użyj interfejsu socket.join API Socket.IO, aby dołączyć klienta docelowego do określonego pokoju.

    /*server.js*/
    socket.on('joinRoom', async (message) => {
        const room_id = message["room_id"];
        await socket.join(room_id);
    });
    
  3. Po dołączeniu klienta serwer informuje klienta o pomyślnym wyniku, wysyłając message zdarzenie. Gdy klient odbiera message zdarzenie o typie ackJoinRoom, klient może poprosić serwer o wysłanie najnowszego stanu edytora.

    /*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. Gdy klient wysyła sendToRoom zdarzenie na serwer, serwer emituje zmiany stanu edytora kodu do określonego pokoju. Wszyscy klienci w pokoju mogą teraz otrzymać najnowszą aktualizację.

    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
        });
    });
    

Pisanie kodu po stronie klienta aplikacji

Po zakończeniu procedur po stronie serwera można pracować po stronie klienta.

Konfiguracja początkowa

Aby komunikować się z serwerem, należy utworzyć klienta Socket.IO. Pytanie brzmi, z którym serwerem klient powinien nawiązać trwałe połączenie. Ponieważ używasz usługi Web PubSub dla Socket.IO, serwer jest usługą platformy Azure. Pamiętaj, że zdefiniowano /negotiate route do obsługi klientów punktu końcowego do web PubSub dla 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];
}

Funkcja initialize(url) organizuje kilka operacji konfiguracji razem:

  • Pobiera punkt końcowy do usługi platformy Azure z serwera HTTP
  • Tworzy wystąpienie edytora Monaco
  • Ustanawia trwałe połączenie z usługą Web PubSub dla Socket.IO

Klient modułu zapisywania

Jak wspomniano wcześniej, po stronie klienta masz dwie role użytkownika: składnik zapisywania i przeglądarki. Wszystkie typy składników zapisywania są przesyłane strumieniowo do ekranu osoby przeglądanej.

  1. Pobierz punkt końcowy do usługi Web PubSub dla Socket.IO i room_id wartości:

    /*client.js*/
    
    let [socket, editor, room_id] = await initialize('/negotiate');
    
  2. Gdy klient składnika zapisywania jest połączony z serwerem, serwer wysyła login zdarzenie do składnika zapisywania. Składnik zapisywania może odpowiedzieć, prosząc serwer o przyłączenie się do określonego pokoju. Co 200 milisekund klient zapisywania wysyła najnowszy stan edytora do pokoju. Funkcja o nazwie flush organizuje logikę wysyłania.

    /*client.js*/
    
    socket.on("login", () => {
        updateStatus('Connected');
        joinRoom(socket, `${room_id}`);
        setInterval(() => flush(), 200);
        // Update editor content
        // ...
    });
    
  3. Jeśli składnik zapisywania nie wprowadza żadnych zmian, flush() nic nie robi i po prostu zwraca. W przeciwnym razie zmiany stanu edytora są wysyłane do pokoju.

    /*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. Po nawiązaniu połączenia nowego klienta przeglądarki przeglądarka musi uzyskać najnowszy pełny stan edytora. Aby to osiągnąć, komunikat zawierający sync dane jest wysyłany do klienta modułu zapisywania. Komunikat prosi klienta modułu zapisywania o wysłanie pełnego stanu edytora.

    /*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,
            });
        }
    });
    

Klient przeglądarki

  1. Podobnie jak klient zapisywania, klient przeglądarki tworzy klienta Socket.IO za pośrednictwem programu initialize(). Gdy klient przeglądarki jest połączony i odbiera login zdarzenie z serwera, prosi serwer o przyłączenie się do określonego pokoju. Zapytanie room_id określa pomieszczenie.

    /*client.js*/
    
    let [socket, editor] = await initialize(`/register?room_id=${room_id}`)
    socket.on("login", () => {
        updateStatus('Connected');
        joinRoom(socket, `${room_id}`);
    });
    
  2. Gdy klient przeglądarki odbiera message zdarzenie z serwera, a typ danych to ackJoinRoom, klient przeglądarki prosi klienta modułu zapisywania w pokoju o wysłanie pełnego stanu edytora.

    /*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. Jeśli typ danych to editorMessage, klient przeglądarki aktualizuje edytor zgodnie z rzeczywistą zawartością.

    /*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. Implementowanie joinRoom() interfejsów API Socket.IO i sendToRoom() przy użyciu nich:

    /*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
        });
    }
    

Uruchamianie aplikacji

Znajdź repozytorium

W poprzednich sekcjach opisano podstawową logikę związaną z synchronizowaniem stanu edytora między przeglądarkami a modułem zapisywania. Pełny kod można znaleźć w repozytorium przykładów.

Klonowanie repozytorium

Możesz sklonować repozytorium i uruchomić npm install polecenie , aby zainstalować zależności projektu.

Uruchamianie serwera

node server.js <web-pubsub-connection-string>

Jest to parametry połączenia otrzymana we wcześniejszym kroku.

Odtwarzanie za pomocą edytora kodu w czasie rzeczywistym

Otwórz na karcie przeglądarki. Otwórz http://localhost:3000 inną kartę z adresem URL wyświetlanym na pierwszej stronie internetowej.

Jeśli napiszesz kod na pierwszej karcie, na drugiej karcie powinno być widoczne wpisywanie odzwierciedlone w czasie rzeczywistym. Usługa Web PubSub dla Socket.IO ułatwia przekazywanie komunikatów w chmurze. Serwer express obsługuje tylko plik statyczny index.html i /negotiate punkt końcowy.