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.

Ważne

Nieprzetworzone parametry połączenia są wyświetlane tylko w tym artykule w celach demonstracyjnych.

parametry połączenia zawiera informacje autoryzacyjne potrzebne Twojej aplikacji, aby uzyskać dostęp do usługi Azure Web PubSub. Klucz dostępu w parametry połączenia działa jak hasło root dla Twojej usługi. W środowiskach produkcyjnych zawsze chroń klucze dostępu. Użyj usługi Azure Key Vault, aby bezpiecznie zarządzać kluczami i obracać je oraz zabezpieczać połączenie za pomocą usługi WebPubSubServiceClient.

Unikaj dystrybuowania kluczy dostępu do innych użytkowników, kodowania ich lub zapisywania ich w dowolnym miejscu w postaci zwykłego tekstu, który jest dostępny dla innych użytkowników. Obracanie kluczy, jeśli uważasz, że mogły one zostać naruszone.

Omówienie

W tym artykule pokazano, jak zbudować aplikację, która umożliwia programiście transmitowanie na żywo swojej pracy programistycznej do publiczności. Tworzysz tę aplikację przy użyciu:

  • Edytor Monaco, edytor kodu, który obsługuje program Visual Studio Code.
  • Express— platforma internetowa Node.js.
  • Interfejsy API udostępniane przez bibliotekę Socket.IO, służące 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.

Zrzut ekranu przedstawiający zakończoną aplikację do przesyłania strumieniowego kodu.

Aby procedury były zwięzłe i łatwe do przyswojenia w około 15 minut, w tym artykule zdefiniowano dwie role użytkownika oraz to, co mogą one robić w edytorze:

  • Autor, który może pisać w edytorze online, a treść jest przesyłana strumieniowo
  • Przeglądający, którzy otrzymują w czasie rzeczywistym treść wpisywaną przez autora i nie mogą jej edytować

Architektura

Towar Cel Ś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 dla 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 pokazujący, jak usługa Web PubSub dla usługi Socket.IO łączy klientów z serwerem.

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

Pobierz ciąg połączenia

Parametr połączenia umożliwia połączenie z usługą Web PubSub przy użyciu Socket.IO.

Uruchom następujące polecenia. Zachowaj gdzieś zwrócony ciąg połączenia, ponieważ będzie potrzebny później w tym artykule, gdy uruchomisz aplikację.

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:

    Nieprzetworzone parametry połączenia są wyświetlane tylko w tym artykule w celach demonstracyjnych. W środowiskach produkcyjnych zawsze chroń klucze dostępu. Użyj usługi Azure Key Vault, aby bezpiecznie zarządzać kluczami i obracać je oraz zabezpieczać połączenie za pomocą usługi WebPubSubServiceClient.

    /*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. Dzięki tym dwóm krokom kod po stronie serwera może przenieść zarządzanie połączeniami stałymi do usługi 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 otrzyma wartość room_id, do którego klient chce dołączyć, użyj metody socket.join z interfejsu API Socket.IO, aby dodać docelowego klienta 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 otrzymuje zdarzenie message typu ackJoinRoom, może poprosić serwer o przesł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 zdarzenie sendToRoom do serwera, serwer rozsyła do wskazanego pokoju zmiany stanu edytora kodu. Wszyscy klienci w tym pokoju mogą teraz otrzymywać najnowsze aktualizacje.

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

Napisz kod aplikacji po stronie klienta

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 przez Ciebie trasę /negotiate, aby udostępniać klientom punkt końcowy usługi 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 usługi platformy Azure z serwera HTTP
  • Tworzy instancję edytora Monaco
  • Ustanawia trwałe połączenie z usługą Web PubSub dla Socket.IO

Klient modułu zapisywania

Jak wspomniano wcześniej, masz po stronie klienta dwie role użytkownika: edytor i przeglądający. Wszystko, co wpisuje autor, jest przesyłane strumieniowo na ekran odbiorcy.

  1. Pobierz punkt końcowy usługi Web PubSub dla Socket.IO oraz wartość room_id:

    /*client.js*/
    
    let [socket, editor, room_id] = await initialize('/negotiate');
    
  2. Gdy klient zapisujący jest połączony z serwerem, serwer wysyła do niego zdarzenie login. Moduł zapisujący może odpowiedzieć, prosząc serwer o dołączenie do określonego pokoju. Co 200 ms klient edytora wysyła do pokoju najnowszy stan edytora. 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 program zapisujący nie wprowadzi żadnych zmian, flush() nic nie robi i po prostu kończy działanie. 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ąć, do klienta zapisującego wysyłany jest komunikat zawierający dane sync. 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 zapisujący, klient wyświetlający tworzy swojego klienta Socket.IO za pośrednictwem initialize(). Gdy klient wyświetlający jest połączony i otrzymuje z serwera zdarzenie login, prosi serwer o dołączenie go 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 wyświetlający odbiera z serwera zdarzenie message, a typ danych to ackJoinRoom, prosi klienta edytującego w danym pokoju o przesł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. Zaimplementuj joinRoom() i sendToRoom(), korzystając z interfejsów API Socket.IO:

    /*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 omówiono główną logikę związaną z synchronizowaniem stanu edytora między użytkownikami przeglądającymi a użytkownikiem edytującym. Pełny kod można znaleźć w repozytorium przykładów.

Sklonuj repozytorium

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

Uruchamianie serwera

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

To są parametry połączenia, które otrzymano we wcześniejszym kroku.

Wypróbuj edytor kodu w czasie rzeczywistym

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

Jeśli piszesz kod w pierwszej karcie, powinieneś widzieć w drugiej karcie w czasie rzeczywistym to, co wpisujesz. 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.