Anleitung zum Erstellen eines WebSocket-Chatdienstes für Cloud Run


In dieser Anleitung erfahren Sie, wie Sie einen Multiroom-Echtzeit-Chatdienst mithilfe von WebSockets mit einer persistenten Verbindung für bidirektionale Kommunikation erstellen. Mit WebSockets können sowohl der Client als auch der Server Nachrichten gegenseitig übertragen, ohne den Server nach Updates abzufragen.

Sie können Cloud Run zwar für die Verwendung der Sitzungsaffinität konfigurieren, dies bietet jedoch eine Best-Effort-Affinität. Dies bedeutet, dass jede neue Anfrage trotzdem möglicherweise an eine andere Instanz weitergeleitet wird. Nutzernachrichten müssen daher im Chatdienst über alle Instanzen hinweg synchronisiert werden, und nicht nur zwischen den Clients, die mit einer Instanz verbunden sind.

Designübersicht

Dieser Beispiel-Chatdienst verwendet eine Memorystore for Redis-Instanz, um Nutzernachrichten in allen Instanzen zu speichern und zu synchronisieren. Redis verwendet einen Pub/Sub-Mechanismus, der nicht mit dem Produkt Cloud Pub/Sub verwechselt werden sollte, um Daten an abonnierte Clients zu senden, die mit einer Instanz verbunden sind, um HTTP-Abfragen zu vermeiden.

Selbst bei Push-Updates erhält jede hochgefahrene Instanz nur neue Nachrichten an den Container. Zum Laden vorheriger Nachrichten muss der Nachrichtenverlauf gespeichert und aus einer nichtflüchtigen Speicherlösung abgerufen werden. In diesem Beispiel werden die konventionellen Funktionen eines Objektspeichers von Redis zum Zwischenspeichern und Abrufen des Nachrichtenverlaufs verwendet.

Architekturdiagramm
Das Diagramm zeigt mehrere Clientverbindungen zu jeder Cloud Run-Instanz. Jede Instanz stellt über einen Connector für serverlosen VPC-Zugriff eine Verbindung zu einer Memorystore for Redis-Instanz her.

Die Redis-Instanz ist über private IP-Adressen mit Zugriffssteuerung geschützt und auf Dienste beschränkt, die im selben virtuellen privaten Netzwerk wie die Redis-Instanz ausgeführt werden. Daher ist ein Connector für serverlosen VPC-Zugriff erforderlich, damit der Cloud Run-Dienst eine Verbindung zu Redis herstellen kann. Weitere Informationen zum Serverlosen VPC-Zugriff.

Beschränkungen

  • In dieser Anleitung wird weder auf Endnutzerauthentifizierungs- oder Sitzungs-Caching eingegangen. Weitere Informationen zur Endnutzerauthentifizierung finden Sie in der Cloud Run-Anleitung zur Endnutzerauthentifizierung.

  • In dieser Anleitung wird keine Datenbank wie Firestore zum unbefristeten Speichern und Abrufen des Chat-Nachrichtenverlaufs implementiert.

  • Für diesen produktionsbereiten Dienst sind zusätzliche Elemente erforderlich. Für die Bereitstellung von Hochverfügbarkeit mit Replikation und automatischem Failover wird eine Redis-Instanz der Standardstufe empfohlen.

Ziele

  • Schreiben, Erstellen und Bereitstellen eines Cloud Run-Dienstes, der WebSockets verwendet.

  • Stellen Sie eine Verbindung zu einer Memorystore for Redis-Instanz her, um neue Nachrichten auf Instanzen zu veröffentlichen und zu abonnieren.

  • Verbinden Sie den Cloud Run-Dienst mit Memorystore über Connectors für den serverlosen VPC-Zugriff.

Kosten

In diesem Dokument verwenden Sie die folgenden kostenpflichtigen Komponenten von Google Cloud:

Mit dem Preisrechner können Sie eine Kostenschätzung für Ihre voraussichtliche Nutzung vornehmen. Neuen Google Cloud-Nutzern steht möglicherweise eine kostenlose Testversion zur Verfügung.

Hinweise

  1. Sign in to your Google Cloud account. If you're new to Google Cloud, create an account to evaluate how our products perform in real-world scenarios. New customers also get $300 in free credits to run, test, and deploy workloads.
  2. In the Google Cloud console, on the project selector page, select or create a Google Cloud project.

    Go to project selector

  3. Make sure that billing is enabled for your Google Cloud project.

  4. In the Google Cloud console, on the project selector page, select or create a Google Cloud project.

    Go to project selector

  5. Make sure that billing is enabled for your Google Cloud project.

  6. Enable the Cloud Run, Memorystore for Redis, Serverless VPC Access, Artifact Registry, and Cloud Build APIs.

    Enable the APIs

  7. Installieren und initialisieren Sie die gcloud CLI.

Erforderliche Rollen

Bitten Sie Ihren Administrator, Ihnen die folgenden IAM-Rollen für Ihr Projekt zuzuweisen, um die Berechtigungen zu erhalten, die Sie zum Ausführen der Anleitung benötigen:

Weitere Informationen zum Zuweisen von Rollen finden Sie unter Zugriff auf Projekte, Ordner und Organisationen verwalten.

Sie können die erforderlichen Berechtigungen auch über benutzerdefinierte Rollen oder andere vordefinierte Rollen erhalten.

gcloud-Standardeinstellungen einrichten

So konfigurieren Sie gcloud mit Standardeinstellungen für den Cloud Run-Dienst:

  1. Legen Sie ein Standardprojekt fest:

    gcloud config set project PROJECT_ID

    Ersetzen Sie PROJECT_ID durch den Namen des Projekts, das Sie für diese Anleitung erstellt haben.

  2. Konfigurieren Sie gcloud für die von Ihnen ausgewählte Region:

    gcloud config set run/region REGION

    Ersetzen Sie REGION durch die unterstützte Cloud Run-Region Ihrer Wahl.

Cloud Run-Standorte

Cloud Run ist regional. Die Infrastruktur, in der die Cloud Run-Dienste ausgeführt werden, befindet sich demnach in einer bestimmten Region. Aufgrund der Verwaltung durch Google sind die Anwendungen in allen Zonen innerhalb dieser Region redundant verfügbar.

Bei der Auswahl der Region, in der Ihre Cloud Run-Dienste ausgeführt werden, ist vorrangig, dass die Anforderungen hinsichtlich Latenz, Verfügbarkeit oder Langlebigkeit erfüllt werden. Sie können im Allgemeinen die Region auswählen, die Ihren Nutzern am nächsten liegt, aber Sie sollten den Standort der anderen Google Cloud-Produkte berücksichtigen, die von Ihrem Cloud Run-Dienst verwendet werden. Die gemeinsame Nutzung von Google Cloud-Produkten an mehreren Standorten kann sich auf die Latenz und die Kosten des Dienstes auswirken.

Cloud Run ist in diesen Regionen verfügbar:

Unterliegt Preisstufe 1

Unterliegt Preisstufe 2

  • africa-south1 (Johannesburg)
  • asia-east2 (Hongkong)
  • asia-northeast3 (Seoul, Südkorea)
  • asia-southeast1 (Singapur)
  • asia-southeast2 (Jakarta)
  • asia-south1 (Mumbai, Indien)
  • asia-south2 (Delhi, Indien)
  • australia-southeast1 (Sydney)
  • australia-southeast2 (Melbourne)
  • europe-central2 (Warschau, Polen)
  • europe-west10 (Berlin)Blattsymbol Niedriger CO2-Ausstoß
  • europe-west12 (Turin)
  • europe-west2 (London, Vereinigtes Königreich) Blattsymbol Niedriger CO2-Ausstoß
  • europe-west3 (Frankfurt, Deutschland) Blattsymbol Niedriger CO2-Ausstoß
  • europe-west6 (Zürich, Schweiz) Blattsymbol Niedriger CO2-Ausstoß
  • me-central1 (Doha)
  • me-central2 (Dammam)
  • northamerica-northeast1 (Montreal) Blattsymbol Niedriger CO2-Ausstoß
  • northamerica-northeast2 (Toronto) Blattsymbol Niedriger CO2-Ausstoß
  • southamerica-east1 (Sao Paulo, Brasilien) Blattsymbol Niedriger CO2-Ausstoß
  • southamerica-west1 (Santiago, Chile) Blattsymbol Niedriger CO2-Ausstoß
  • us-west2 (Los Angeles)
  • us-west3 (Salt Lake City)
  • us-west4 (Las Vegas)

Wenn Sie bereits einen Cloud Run-Dienst erstellt haben, können Sie dessen Region im Cloud Run-Dashboard der Google Cloud Console aufrufen.

Codebeispiel abrufen

So rufen Sie das gewünschte Codebeispiel ab:

  1. Klonen Sie das Beispiel-Repository auf Ihren lokalen Computer:

    Node.js

    git clone https://github.com/GoogleCloudPlatform/nodejs-docs-samples.git

    Sie können auch das Beispiel als ZIP-Datei herunterladen und extrahieren.

  2. Wechseln Sie in das Verzeichnis, das den Cloud Run-Beispielcode enthält:

    Node.js

    cd nodejs-docs-samples/run/websockets/

Code verstehen

Socket.io ist eine Bibliothek, die eine bidirektionale Echtzeitkommunikation zwischen Browser und Server ermöglicht. Obwohl Socket.io keine WebSocket-Implementierung ist, umfasst es die Funktion, eine einfachere API für mehrere Kommunikationsprotokolle mit zusätzlichen Features, wie verbesserter Zuverlässigkeit, automatischer Neuverbindung sowie Broadcasting an einen oder alle Clients, bereitzustellen.

Clientseitige Integration

<script src="/socket.io/socket.io.js"></script>

Der Client instanziiert eine neue Socket-Instanz für jede Verbindung. Da dieses Beispiel serverseitig gerendert wird, muss die Server-URL nicht definiert werden. Die Socket-Instanz kann Ereignisse ausgeben und abhören.

// Initialize Socket.io
const socket = io('', {
  transports: ['websocket'],
});
// Emit "sendMessage" event with message
socket.emit('sendMessage', msg, error => {
  if (error) {
    console.error(error);
  } else {
    // Clear message
    $('#msg').val('');
  }
});
// Listen for new messages
socket.on('message', msg => {
  log(msg.user, msg.text);
});

// Listen for notifications
socket.on('notification', msg => {
  log(msg.title, msg.description);
});

// Listen connect event
socket.on('connect', () => {
  console.log('connected');
});

Serverseitige Integration

Auf der Serverseite wird der Socket.io-Server initialisiert und mit dem HTTP-Server verbunden. Ähnlich wie auf der Clientseite wird nach dem Herstellen einer Verbindung zum Client mit Socket.io eine Socket-Instanz für jede Verbindung erstellt, mit Nachrichten ausgegeben und überwacht werden können. Socket.io bietet auch eine einfache Schnittstelle zum Erstellen von "Chatrooms" oder einem beliebigen Kanal, dem Sockets beitreten und ihn verlassen können.

// Initialize Socket.io
const server = require('http').Server(app);
const io = require('socket.io')(server);

const {createAdapter} = require('@socket.io/redis-adapter');
// Replace in-memory adapter with Redis
const subClient = redisClient.duplicate();
io.adapter(createAdapter(redisClient, subClient));
// Add error handlers
redisClient.on('error', err => {
  console.error(err.message);
});

subClient.on('error', err => {
  console.error(err.message);
});

// Listen for new connection
io.on('connection', socket => {
  // Add listener for "signin" event
  socket.on('signin', async ({user, room}, callback) => {
    try {
      // Record socket ID to user's name and chat room
      addUser(socket.id, user, room);
      // Call join to subscribe the socket to a given channel
      socket.join(room);
      // Emit notification event
      socket.in(room).emit('notification', {
        title: "Someone's here",
        description: `${user} just entered the room`,
      });
      // Retrieve room's message history or return null
      const messages = await getRoomFromCache(room);
      // Use the callback to respond with the room's message history
      // Callbacks are more commonly used for event listeners than promises
      callback(null, messages);
    } catch (err) {
      callback(err, null);
    }
  });

  // Add listener for "updateSocketId" event
  socket.on('updateSocketId', async ({user, room}) => {
    try {
      addUser(socket.id, user, room);
      socket.join(room);
    } catch (err) {
      console.error(err);
    }
  });

  // Add listener for "sendMessage" event
  socket.on('sendMessage', (message, callback) => {
    // Retrieve user's name and chat room  from socket ID
    const {user, room} = getUser(socket.id);
    if (room) {
      const msg = {user, text: message};
      // Push message to clients in chat room
      io.in(room).emit('message', msg);
      addMessageToCache(room, msg);
      callback();
    } else {
      callback('User session not found.');
    }
  });

  // Add listener for disconnection
  socket.on('disconnect', () => {
    // Remove socket ID from list
    const {user, room} = deleteUser(socket.id);
    if (user) {
      io.in(room).emit('notification', {
        title: 'Someone just left',
        description: `${user} just left the room`,
      });
    }
  });
});

Socket.io stellt auch einen Redis-Adapter bereit, um Ereignisse an alle Clients zu senden, unabhängig davon, welcher Server den Socket bereitstellt. Socket.io verwendet ausschließlich den Pub/Sub-Mechanismus von Redis und speichert keine Daten.

const {createAdapter} = require('@socket.io/redis-adapter');
// Replace in-memory adapter with Redis
const subClient = redisClient.duplicate();
io.adapter(createAdapter(redisClient, subClient));

Der Redis-Adapter von Socket.io kann den Redis-Client wiederverwenden, der zum Speichern des Nachrichtenverlaufs des Chatrooms verwendet wird. Jeder Container erstellt eine Verbindung zur Redis-Instanz und Cloud Run kann eine große Anzahl von Instanzen erstellen. Das sind deutlich weniger als die 65.000 Verbindungen, die Redis unterstützen kann. Wenn Sie diese Menge an Traffic unterstützen müssen, müssen Sie auch den Durchsatz des Connectors für den serverlosen VPC-Zugriff auswerten.

Verbindung wiederherstellen

Cloud Run hat eine maximale Zeitüberschreitung von 60 Minuten. Daher müssen Sie eine erneute Verbindungslogik für mögliche Zeitlimits hinzufügen. In einigen Fällen versucht Socket.io automatisch, die Verbindung nach einem Verbindungs- oder Verbindungsfehler wiederherzustellen. Es gibt keine Garantie dafür, dass der Client wieder eine Verbindung zur selben Instanz herstellt.

// Listen for reconnect event
socket.io.on('reconnect', () => {
  console.log('reconnected');
  // Emit "updateSocketId" event to update the recorded socket ID with user and room
  socket.emit('updateSocketId', {user, room}, error => {
    if (error) {
      console.error(error);
    }
  });
});
// Add listener for "updateSocketId" event
socket.on('updateSocketId', async ({user, room}) => {
  try {
    addUser(socket.id, user, room);
    socket.join(room);
  } catch (err) {
    console.error(err);
  }
});

Instanzen bleiben bei einer aktiven Verbindung bestehen, bis alle Anfragen geschlossen oder das Zeitlimit überschritten werden. Auch wenn Sie die Sitzungsaffinität von Cloud Run verwenden, können neue Anfragen mit Load-Balancing auf aktive Container verteilt werden, sodass Container herunterskaliert werden können. Wenn Sie befürchten, dass eine große Anzahl von Containern nach einer Traffic-Spitze bestehen bleibt, können Sie den maximalen Zeitüberschreitungswert verringern, damit nicht verwendete Sockets häufiger bereinigt werden.

Dienst versenden

  1. Erstellen einer Instanz von Memorystore for Redis:

    gcloud redis instances create INSTANCE_ID --size=1 --region=REGION

    Ersetzen Sie INSTANCE_ID durch den Namen der Instanz, z. B. my-redis-instance, und REGION_ID durch die Region für alle Ihre Ressourcen und Dienste, z. B. us-central1.

    Der Instanz wird automatisch ein IP-Bereich aus dem Standarddienstnetzwerkbereich zugewiesen. In dieser Anleitung wird für den lokalen Cache von Nachrichten in der Redis-Instanz 1 GB Arbeitsspeicher verwendet. Weitere Informationen zum Bestimmen der anfänglichen Größe einer Memorystore-Instanz für Ihren Anwendungsfall.

  2. Richten Sie einen Connector für serverlosen VPC-Zugriff ein.

    Zum Herstellen einer Verbindung zu Ihrer Redis-Instanz benötigt Ihr Cloud Run-Dienst Zugriff auf das autorisierte VPC-Netzwerk der Redis-Instanz.

    Jeder VPC-Connector benötigt ein eigenes /28-Subnetz, um Connector-Instanzen zu platzieren. Dieser IP-Bereich darf sich nicht mit vorhandenen IP-Adressreservierungen im VPC-Netzwerk überschneiden. 10.8.0.0 (/28) funktioniert beispielsweise in den meisten neuen Projekten oder Sie können einen anderen nicht verwendeten benutzerdefinierten IP-Bereich wie 10.9.0.0 angeben (/28). Sie können in der Google Cloud Console sehen, welche IP-Bereiche derzeit reserviert sind.

    gcloud compute networks vpc-access connectors create CONNECTOR_NAME \
      --region REGION \
      --range "10.8.0.0/28"

    Ersetzen Sie CONNECTOR_NAME durch den Namen des Connectors.

    Dieser Befehl erstellt einen Connector im Standard-VPC-Netzwerk, genauso wie die Redis-Instanz, mit der Maschinengröße e2-micro. Wenn Sie die Maschinengröße des Connectors erhöhen, kann dadurch der Durchsatz erhöht werden, allerdings steigen auch die Kosten. Der Connector muss sich außerdem in derselben Region wie die Redis-Instanz befinden. Serverlosen VPC-Zugriff konfigurieren

  3. Definieren Sie eine Umgebungsvariable mit der IP-Adresse des autorisierten Netzwerks der Redis-Instanz:

     export REDISHOST=$(gcloud redis instances describe INSTANCE_ID --region REGION --format "value(host)")
  4. Erstellen Sie ein Dienstkonto, das als Dienstidentität dient. Standardmäßig hat dieses keine anderen Berechtigungen als die Projektmitgliedschaft.

    gcloud iam service-accounts create chat-identity
    gcloud projects add-iam-policy-binding PROJECT_ID \
    --member=serviceAccount:chat-identity@PROJECT_ID.iam.gserviceaccount.com \
    --role=roles/serviceusage.serviceUsageConsumer
  5. Erstellen Sie das Container-Image und stellen Sie es in Cloud Run bereit:

    gcloud run deploy chat-app --source . \
        --vpc-connector CONNECTOR_NAME \
        --allow-unauthenticated \
        --timeout 3600 \
        --service-account chat-identity \
        --update-env-vars REDISHOST=$REDISHOST

    Beantworten Sie die Aufforderungen, die erforderlichen APIs zu installieren, indem Sie nach Aufforderung y antworten. Dies ist nur einmal für ein Projekt erforderlich. Antworten Sie auf andere Aufforderungen, indem Sie die Plattform und Region angeben, sofern Sie diese nicht wie auf der Einrichtungsseite beschrieben eingerichtet haben. Weitere Informationen zum Aus Quellcode bereitstellen.

Testen

So testen Sie den gesamten Dienst:

  1. Rufen Sie im Browser die URL auf, die Sie im oben beschriebenen Bereitstellungsschritt erhalten haben.

  2. Geben Sie Ihren Namen und einen Chatroom ein, um sich anzumelden.

  3. Nachricht an den Chatroom senden.

Wenn Sie sich dafür entscheiden, mit der Entwicklung dieser Dienste fortzufahren, denken Sie daran, dass diese nur eingeschränkten IAM-Zugriff (Identity and Access Management) auf den Rest von Google Cloud haben und ihnen zusätzliche IAM-Rollen zugewiesen werden müssen, um auf viele andere Dienste zugreifen zu können.

Bereinigen

Wenn Sie ein neues Projekt für diese Anleitung erstellt haben, löschen Sie das Projekt. Wenn Sie ein vorhandenes Projekt verwendet haben und es beibehalten möchten, ohne die Änderungen in dieser Anleitung hinzuzufügen, löschen Sie die für die Anleitung erstellten Ressourcen.

Projekt löschen

Am einfachsten vermeiden Sie weitere Kosten, wenn Sie das zum Ausführen der Anleitung erstellte Projekt löschen.

So löschen Sie das Projekt:

  1. In the Google Cloud console, go to the Manage resources page.

    Go to Manage resources

  2. In the project list, select the project that you want to delete, and then click Delete.
  3. In the dialog, type the project ID, and then click Shut down to delete the project.

Anleitungsressourcen löschen

  1. Löschen Sie den Cloud Run-Dienst, den Sie in dieser Anleitung bereitgestellt haben:

    gcloud run services delete SERVICE-NAME

    Dabei ist SERVICE-NAME der von Ihnen ausgewählte Dienstname.

    Sie können Cloud Run-Dienste auch über die Google Cloud Console löschen.

  2. Entfernen Sie die Konfiguration der Standardregion gcloud, die Sie während der Einrichtung für die Anleitung hinzugefügt haben:

     gcloud config unset run/region
    
  3. Entfernen Sie die Projektkonfiguration:

     gcloud config unset project
    
  4. Löschen Sie sonstige Google Cloud-Ressourcen, die in dieser Anleitung erstellt wurden:

Nächste Schritte