Tutorial membangun layanan Chat WebSocket untuk Cloud Run


Tutorial ini menunjukkan cara membuat layanan chat real-time multi-ruangan menggunakan WebSockets dengan koneksi persisten untuk komunikasi dua arah. Dengan WebSockets, klien dan server dapat mengirim pesan satu sama lain tanpa melakukan polling di server untuk update.

Meskipun Anda dapat mengonfigurasi Cloud Run untuk menggunakan afinitas sesi, hal ini memberikan afinitas upaya terbaik, yang berarti bahwa setiap permintaan baru masih dapat berpotensi dirutekan ke instance yang berbeda. Akibatnya, pesan pengguna dalam layanan chat perlu disinkronkan di semua instance, bukan hanya antara klien yang terhubung ke satu instance.

Ringkasan desain

Layanan chat contoh ini menggunakan instance Memorystore untuk Redis untuk menyimpan dan menyinkronkan pesan pengguna di semua instance. Redis menggunakan mekanisme Pub/Sub, berbeda dengan produk Cloud Pub/Sub, untuk mengirim data ke klien yang berlangganan yang terhubung ke instance apa pun, guna menghilangkan polling HTTP untuk update.

Namun, meskipun dengan update push, instance apa pun yang dijalankan hanya akan menerima pesan baru yang dikirim ke container. Untuk memuat pesan sebelumnya, histori pesan harus disimpan dan diambil dari solusi penyimpanan persisten. Contoh ini menggunakan fungsi konvensional Redis tentang penyimpanan objek untuk meng-cache dan mengambil histori pesan.

Diagram Arsitektur
Diagram ini menunjukkan beberapa koneksi klien ke setiap instance Cloud Run. Setiap instance terhubung ke instance Memorystore untuk Redis melalui konektor Akses VPC Serverless.

Instance Redis terlindungi dari internet menggunakan IP pribadi dengan akses yang dikontrol dan terbatas untuk layanan yang berjalan pada Virtual Private Network yang sama dengan instance Redis; oleh karena itu, konektor Akses VPC Serverless diperlukan agar layanan Cloud Run dapat terhubung ke Redis. Pelajari lebih lanjut mengenai Akses VPC Serverless.

Batasan

  • Tutorial ini tidak menunjukkan autentikasi pengguna akhir atau cache sesi. Untuk mempelajari autentikasi pengguna akhir lebih lanjut, lihat tutorial Cloud Run untuk autentikasi pengguna akhir.

  • Tutorial ini tidak menerapkan database seperti Firestore untuk penyimpanan tanpa batas dan pengambilan histori pesan chat.

  • Elemen tambahan diperlukan agar layanan contoh ini siap produksi. Sebaiknya gunakan Instance Redis Tingkat Standar untuk menyediakan Ketersediaan Tinggi menggunakan replikasi dan failover otomatis.

Tujuan

  • Menulis, membangun, dan men-deploy layanan Cloud Run yang menggunakan WebSockets.

  • Menghubungkan ke instance Memorystore untuk Redis untuk memublikasikan dan berlangganan ke pesan baru di berbagai instance.

  • Menghubungkan layanan Cloud Run dengan Memorystore menggunakan konektor Akses VPC Serverless.

Biaya

Dalam dokumen ini, Anda menggunakan komponen Google Cloud yang dapat ditagih berikut:

Untuk membuat perkiraan biaya berdasarkan proyeksi penggunaan Anda, gunakan kalkulator harga. Pengguna baru Google Cloud mungkin memenuhi syarat untuk mendapatkan uji coba gratis.

Sebelum memulai

  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. Di konsol Google Cloud, pada halaman pemilih project, pilih atau buat project Google Cloud.

    Buka pemilih project

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

  4. Di konsol Google Cloud, pada halaman pemilih project, pilih atau buat project Google Cloud.

    Buka pemilih project

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

  6. Aktifkan API Cloud Run, Memorystore for Redis, Serverless VPC Access, Artifact Registry, and Cloud Build .

    Mengaktifkan API

  7. Instal dan lakukan inisialisasi gcloud CLI.

Peran yang diperlukan

Untuk mendapatkan izin yang Anda perlukan untuk menyelesaikan tutorial, minta administrator Anda untuk memberi Anda peran IAM berikut di project Anda:

Untuk mengetahui informasi selengkapnya tentang cara memberikan peran, lihat Mengelola akses ke project, folder, dan organisasi.

Anda mungkin juga bisa mendapatkan izin yang diperlukan melalui peran khusus atau peran bawaan lainnya.

Menyiapkan default gcloud

Untuk mengonfigurasi gcloud dengan setelan default untuk layanan Cloud Run Anda:

  1. Setel project default Anda:

    gcloud config set project PROJECT_ID

    Ganti PROJECT_ID dengan nama project yang Anda buat untuk tutorial ini.

  2. Konfigurasi gcloud untuk region yang Anda pilih:

    gcloud config set run/region REGION

    Ganti REGION dengan region Cloud Run pilihan Anda yang didukung.

Lokasi Cloud Run

Cloud Run bersifat regional, berarti infrastruktur yang menjalankan layanan Cloud Run Anda terletak di region tertentu dan dikelola oleh Google agar tersedia secara redundan di semua zona dalam region tersebut.

Memenuhi persyaratan latensi, ketersediaan, atau ketahanan adalah faktor utama untuk memilih region tempat layanan Cloud Run dijalankan. Pada umumnya, Anda dapat memilih region yang paling dekat dengan pengguna Anda, tetapi Anda harus mempertimbangkan lokasi dari produk Google Cloud lain yang digunakan oleh layanan Cloud Run Anda. Menggunakan produk Google Cloud secara bersamaan di beberapa lokasi dapat memengaruhi latensi serta biaya layanan Anda.

Cloud Run tersedia di region berikut:

Tergantung harga Tingkat 1

  • asia-east1 (Taiwan)
  • asia-northeast1 (Tokyo)
  • asia-northeast2 (Osaka)
  • asia-south1 (Mumbai, India)
  • europe-north1 (Finlandia) ikon daun CO2 Rendah
  • europe-southwest1 (Madrid) ikon daun CO2 Rendah
  • europe-west1 (Belgia) ikon daun CO2 Rendah
  • europe-west4 (Belanda) ikon daun CO2 Rendah
  • europe-west8 (Milan)
  • europe-west9 (Paris) ikon daun CO2 Rendah
  • me-west1 (Tel Aviv)
  • us-central1 (Iowa) ikon daun CO2 Rendah
  • us-east1 (South Carolina)
  • us-east4 (North Virginia)
  • us-east5 (Columbus)
  • us-south1 (Dallas) ikon daun CO2 Rendah
  • us-west1 (Oregon) ikon daun CO2 Rendah

Tergantung harga Tingkat 2

  • africa-south1 (Johannesburg)
  • asia-east2 (Hong Kong)
  • asia-northeast3 (Seoul, Korea Selatan)
  • asia-southeast1 (Singapura)
  • asia-southeast2 (Jakarta)
  • asia-south2 (Delhi, India)
  • australia-southeast1 (Sydney)
  • australia-southeast2 (Melbourne)
  • europe-central2 (Warsawa, Polandia)
  • europe-west10 (Berlin) ikon daun CO2 Rendah
  • europe-west12 (Turin)
  • europe-west2 (London, Inggris Raya) ikon daun CO2 Rendah
  • europe-west3 (Frankfurt, Jerman) ikon daun CO2 Rendah
  • europe-west6 (Zurich, Swiss) ikon daun CO2 Rendah
  • me-central1 (Doha)
  • me-central2 (Dammam)
  • northamerica-northeast1 (Montreal) ikon daun CO2 Rendah
  • northamerica-northeast2 (Toronto) ikon daun CO2 Rendah
  • southamerica-east1 (São Paulo, Brasil) ikon daun CO2 Rendah
  • southamerica-west1 (Santiago, Cile) ikon daun CO2 Rendah
  • us-west2 (Los Angeles)
  • us-west3 (Salt Lake City)
  • us-west4 (Las Vegas)

Jika sudah membuat layanan Cloud Run, Anda dapat melihat region di dasbor Cloud Run di Konsol Google Cloud.

Mengambil contoh kode

Untuk mengambil contoh kode agar dapat digunakan:

  1. Clone repositori contoh ke komputer lokal Anda:

    Node.js

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

    Atau, Anda dapat mendownload contoh sebagai file ZIP dan mengekstraknya.

  2. Ubah ke direktori yang memuat kode contoh Cloud Run:

    Node.js

    cd nodejs-docs-samples/run/websockets/

Memahami kode

Socket.io adalah library yang memungkinkan komunikasi dua arah secara real-time antara browser dan server. Meskipun bukan implementasi WebSocket, Socket.io menggabungkan fungsi untuk menyediakan API yang lebih sederhana bagi beberapa protokol komunikasi dengan fitur tambahan seperti keandalan yang lebih baik, sambungan ulang otomatis, dan menyiarkan pesan ke semua atau sebagian klien.

Integrasi sisi klien

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

Klien membuat instance instance Socket baru untuk setiap koneksi. Karena contoh ini dirender di sisi server, URL server tidak perlu ditentukan. Instance soket dapat memunculkan dan memproses peristiwa.

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

Integrasi sisi server

Di sisi server, server Socket.io diinisialisasi dan dipasang ke server HTTP. Mirip dengan sisi klien, setelah server Socket.io membuat koneksi ke klien, instance socket dibuat untuk setiap koneksi yang dapat digunakan untuk memunculkan dan memproses pesan. Socket.io juga menyediakan antarmuka yang mudah untuk membuat "ruang" atau saluran arbitrer tempat soket dapat bergabung dan keluar.

// 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 juga menyediakan adaptor Redis untuk menyiarkan peristiwa ke semua klien, terlepas dari server mana yang melayani soket. Socket.io hanya menggunakan mekanisme Pub/Sub Redis dan tidak menyimpan data apa pun.

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

Adaptor Redis Socket.io dapat menggunakan kembali klien Redis yang digunakan untuk menyimpan histori pesan ruang. Setiap container akan membuat koneksi ke instance Redis dan Cloud Run dapat membuat instance dalam jumlah besar. Koneksi ini jauh di bawah 65000 koneksi yang dapat didukung Redis. Jika Anda perlu mendukung jumlah traffic ini, Anda juga perlu mengevaluasi throughput konektor Akses VPC Serverless.

Sambungan ulang

Cloud Run memiliki waktu tunggu maksimum 60 menit. Jadi, Anda perlu menambahkan logika sambungan ulang untuk kemungkinan waktu tunggu. Dalam beberapa kasus, Socket.io secara otomatis mencoba menghubungkan kembali setelah peristiwa pemutusan koneksi atau error koneksi. Tidak ada jaminan bahwa klien akan terhubung kembali ke instance yang sama.

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

Instance akan tetap ada jika ada koneksi yang aktif hingga semua permintaan ditutup atau waktu habis. Meskipun Anda menggunakan afinitas sesi Cloud Run, permintaan baru dapat di-load balanced ke container aktif, sehingga container dapat menurunkan skala. Jika Anda khawatir mengenai banyaknya container yang terus ada setelah terjadi lonjakan traffic, Anda dapat menurunkan nilai waktu tunggu maksimum agar soket yang tidak digunakan lebih sering dibersihkan.

Mengirimkan layanan

  1. Buat instance Memorystore untuk Redis:

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

    Ganti INSTANCE_ID dengan nama instance, yaitu my-redis-instance, dan REGION_ID dengan region untuk semua resource dan layanan Anda, yaitu us-central1.

    Instance akan secara otomatis mendapatkan alokasi rentang IP dari rentang jaringan layanan default. Tutorial ini menggunakan memori 1 GB untuk cache lokal pesan dalam instance Redis. Pelajari lebih lanjut cara Menentukan ukuran awal instance Memorystore untuk kasus penggunaan Anda.

  2. Siapkan konektor Akses VPC Serverless:

    Agar terhubung ke instance Redis, layanan Cloud Run Anda memerlukan akses ke jaringan VPC resmi instance Redis.

    Setiap konektor VPC memerlukan subnet /28 tersendiri untuk menempatkan instance konektor. Rentang IP ini tidak boleh tumpang tindih dengan reservasi alamat IP apa pun yang sudah ada di jaringan VPC Anda. Contohnya, 10.8.0.0 (/28) akan berfungsi di sebagian besar project baru atau Anda dapat menentukan rentang IP kustom lain yang tidak digunakan, seperti 10.9.0.0 (/28). Anda dapat melihat rentang IP yang saat ini dicadangkan di Konsol Google Cloud.

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

    Ganti CONNECTOR_NAME dengan nama untuk konektor Anda.

    Perintah ini membuat konektor di jaringan VPC default, seperti instance Redis, dengan ukuran mesin e2-micro. Meningkatkan ukuran mesin konektor dapat meningkatkan throughput konektor, tetapi juga akan meningkatkan biaya. Konektor juga harus berada di region yang sama dengan instance Redis. Pelajari lebih lanjut cara Mengonfigurasi Akses VPC Serverless.

  3. Tentukan variabel lingkungan dengan alamat IP jaringan yang diizinkan instance Redis:

     export REDISHOST=$(gcloud redis instances describe INSTANCE_ID --region REGION --format "value(host)")
  4. Buat akun layanan untuk dijadikan sebagai identitas layanan. Secara default, opsi ini tidak memiliki hak istimewa selain keanggotaan project.

    gcloud iam service-accounts create chat-identity
    gcloud projects add-iam-policy-binding PROJECT_ID \
    --member=serviceAccount:chat-identity@PROJECT_ID. \
    --role=roles/serviceusage.serviceUsageConsumer
  5. Bangun dan deploy image container ke Cloud Run:

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

    Respons setiap permintaan untuk menginstal API yang diperlukan dengan merespons y saat diminta. Anda hanya perlu melakukan ini sekali untuk sebuah project. Respons permintaan lain dengan menyediakan platform dan region, jika Anda belum menetapkan setelan default untuk perintah tersebut, seperti yang dijelaskan di halaman penyiapan. Pelajari lebih lanjut cara Men-deploy dari kode sumber.

Melakukan Percobaan

Untuk mencoba layanan lengkap:

  1. Buka browser Anda ke URL yang disediakan pada langkah deployment di atas.

  2. Tambahkan nama Anda dan ruang chat untuk login.

  3. Kirim pesan ke ruang.

Jika Anda memilih untuk terus mengembangkan layanan ini, perlu diingat bahwa layanan tersebut telah membatasi akses Identity and Access Management (IAM) ke bagian lain Google Cloud dan perlu diberi peran IAM tambahan untuk mengakses banyak layanan lainnya.

Pembersihan

Jika Anda membuat project baru untuk tutorial ini, hapus project tersebut. Jika Anda menggunakan project yang ada dan ingin mempertahankannya tanpa ada perubahan yang ditambahkan dalam tutorial ini, hapus resource yang dibuat untuk tutorial.

Menghapus project

Cara termudah untuk menghilangkan penagihan adalah dengan menghapus project yang Anda buat untuk tutorial.

Untuk menghapus project:

  1. Di konsol Google Cloud, buka halaman Manage resource.

    Buka Manage resource

  2. Pada daftar project, pilih project yang ingin Anda hapus, lalu klik Delete.
  3. Pada dialog, ketik project ID, lalu klik Shut down untuk menghapus project.

Menghapus resource tutorial

  1. Hapus layanan Cloud Run yang Anda deploy dalam tutorial ini:

    gcloud run services delete SERVICE-NAME

    Dengan SERVICE-NAME adalah nama layanan pilihan Anda.

    Anda juga dapat menghapus layanan Cloud Run dari Konsol Google Cloud.

  2. Hapus konfigurasi region default gcloud yang Anda tambahkan selama penyiapan tutorial:

     gcloud config unset run/region
    
  3. Hapus konfigurasi project:

     gcloud config unset project
    
  4. Hapus resource Google Cloud lain yang dibuat dalam tutorial ini:

Langkah berikutnya