建構 Cloud Run 教學課程的 WebSocket 即時通訊服務

本教學課程說明如何使用 WebSockets 建立多聊天室即時通訊服務,並透過永久連線進行雙向通訊。有了 WebSockets,用戶端和伺服器都能互相推送訊息,不必輪詢伺服器來取得更新。

雖然您可以將 Cloud Run 設為使用工作階段相依性,但這項功能會提供盡力而為的相依性,也就是說,任何新要求仍有可能轉送至其他執行個體。因此,聊天服務中的使用者訊息必須在所有執行個體之間同步處理,而不只是在連線至一個執行個體的用戶端之間同步處理。

設計即時通訊服務

這個範例即時通訊服務使用 Memorystore for Redis 執行個體,在所有執行個體中儲存及同步處理使用者訊息。Redis 使用 Pub/Sub 機制 (請勿與 Cloud Pub/Sub 產品混淆),將資料推送至連線至任何執行個體的訂閱用戶端,以消除 HTTP 輪詢更新。

不過,即使採用推送更新,任何啟動的執行個體也只會收到推送至容器的新訊息。如要載入先前的訊息,必須將訊息記錄儲存至永久儲存空間解決方案,並從中擷取。這個範例使用 Redis 的物件儲存庫傳統功能,快取及擷取訊息記錄。

Redis 執行個體使用私人 IP,可防範網際網路中的各種威脅,並控管及限制存取權,僅允許在與 Redis 執行個體相同的虛擬私人網路中執行的服務存取。建議您使用直連虛擬私有雲輸出。

限制

  • 本教學課程不會說明使用者驗證或工作階段快取。如要進一步瞭解使用者驗證,請參閱 Cloud Run 的使用者驗證教學課程。

  • 本教學課程不會實作 Firestore 等資料庫,用於無限期儲存及擷取即時通訊訊息記錄。

  • 這項範例服務需要額外元素,才能用於正式環境。建議使用標準級 Redis 執行個體,透過複製和自動容錯移轉功能提供高可用性。

目標

  • 編寫、建構及部署使用 WebSocket 的 Cloud Run 服務。

  • 連線至 Memorystore for Redis 執行個體,在執行個體之間發布及訂閱新訊息。

  • 使用直接虛擬私有雲輸出,將 Cloud Run 服務與 Memorystore 連結。

費用

在本文件中,您會使用下列 Google Cloud計費元件:

您可以使用 Pricing Calculator,根據預測用量估算費用。

初次使用 Google Cloud 的使用者可能符合免費試用期資格。

事前準備

  1. 登入 Google Cloud 帳戶。如果您未曾使用過 Google Cloud,歡迎 建立帳戶,親自體驗實際使用我們產品的成效。新客戶還能獲得價值 $300 美元的免費抵免額,用於執行、測試及部署工作負載。
  2. In the Google Cloud console, on the project selector page, select or create a Google Cloud project.

    Roles required to select or create a project

    • Select a project: Selecting a project doesn't require a specific IAM role—you can select any project that you've been granted a role on.
    • Create a project: To create a project, you need the Project Creator role (roles/resourcemanager.projectCreator), which contains the resourcemanager.projects.create permission. Learn how to grant roles.

    Go to project selector

  3. Verify 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.

    Roles required to select or create a project

    • Select a project: Selecting a project doesn't require a specific IAM role—you can select any project that you've been granted a role on.
    • Create a project: To create a project, you need the Project Creator role (roles/resourcemanager.projectCreator), which contains the resourcemanager.projects.create permission. Learn how to grant roles.

    Go to project selector

  5. Verify that billing is enabled for your Google Cloud project.

  6. 啟用 Cloud Run、Memorystore for Redis、Artifact Registry 和 Cloud Build API (如有尚未啟用的 API)。

    啟用 API 所需的角色

    您必須具備 serviceusage.services.enable 權限,才能啟用 API。如果您建立了專案,可能已透過「擁有者」角色 (roles/owner) 取得這項權限。否則,您可以透過「Service Usage 管理員」角色 (roles/serviceusage.serviceUsageAdmin) 取得這項權限。瞭解如何授予角色。

    啟用 API

  7. 安裝並初始化 gcloud CLI。

必要的角色

如要取得完成教學課程所需的權限,請要求管理員在專案中授予您下列 IAM 角色:

如要進一步瞭解如何授予角色,請參閱「管理專案、資料夾和組織的存取權」。

您或許也能透過自訂角色或其他預先定義的角色,取得必要權限。

設定 gcloud 預設值

如要針對 Cloud Run 服務設定 gcloud 的預設值:

  1. 設定您的預設專案:

    gcloud config set project PROJECT_ID

    將 PROJECT_ID 替換為您為本教學課程建立的專案名稱。

  2. 為所選區域設定 gcloud:

    gcloud config set run/region REGION

    將 REGION 替換為您選擇的支援 Cloud Run 區域。

Cloud Run 位置

Cloud Run 具有區域性,這表示執行 Cloud Run 服務的基礎架構位於特定區域,並由 Google 負責管理,實現該區域內所有可用區之間的備援可用性。

選擇執行 Cloud Run 服務的區域時,主要考量重點在於延遲時間、可用性或耐用性需求。原則上可以選擇距離使用者最近的區域,但也應考慮 Cloud Run 服務所用其他 Google Cloud產品的位置。如果跨多個位置使用 Google Cloud 產品,可能會影響服務的延遲時間和費用。

Cloud Run 可在下列地區使用:

採用級別 1 定價

採用級別 2 定價

  • africa-south1 (約翰尼斯堡)
  • asia-east2 (香港)
  • asia-northeast3 (韓國首爾)
  • asia-southeast1 (新加坡)
  • asia-southeast2 (雅加達)
  • asia-south2 (印度德里)
  • australia-southeast1 (雪梨)
  • australia-southeast2 (墨爾本)
  • europe-central2 (波蘭華沙) 節能綠葉圖示 低二氧化碳
  • europe-west10 (柏林)
  • europe-west12 (杜林) 節能綠葉圖示 低二氧化碳排放
  • europe-west2 (英國倫敦) 節能綠葉圖示 低二氧化碳排放
  • europe-west3 (德國法蘭克福)
  • europe-west6 (瑞士蘇黎世) 節能綠葉圖示 低二氧化碳
  • me-central1 (杜哈)
  • me-central2 (達曼)
  • northamerica-northeast1 (蒙特婁) 節能綠葉圖示 低二氧化碳排放
  • northamerica-northeast2 (多倫多) 節能綠葉圖示 低二氧化碳排放
  • southamerica-east1 (巴西聖保羅) 節能綠葉圖示 低二氧化碳排放
  • southamerica-west1 (智利聖地牙哥) 節能綠葉圖示 低二氧化碳排放
  • us-west2 (洛杉磯)
  • us-west3 (鹽湖城)
  • us-west4 (拉斯維加斯)

如果您已建立 Cloud Run 服務,可以在Google Cloud 控制台的 Cloud Run 資訊主頁中查看區域。

擷取程式碼範例

如要擷取要使用的程式碼範例:

  1. 將範例存放區複製到本機電腦中:

    Node.js

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

    您也可以 下載範例 ZIP 檔案,然後解壓縮。

  2. 變更為包含 Cloud Run 程式碼範例的目錄:

    Node.js

    cd nodejs-docs-samples/run/websockets/

瞭解 WebSocket 程式碼

Socket.io 程式庫可在瀏覽器和伺服器之間,建立即時雙向通訊。雖然 Socket.io 不是 WebSocket 實作,但它確實包裝了功能,為多個通訊協定提供更簡單的 API,並提供額外的功能,例如提高可靠性、自動重新連線,以及向所有或部分用戶端廣播。

用戶端整合

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

用戶端會為每個連線例項化新的通訊端例項。由於這個範例是在伺服器端算繪,因此不需要定義伺服器網址。Socket 執行個體可以發出及監聽事件。

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

伺服器端整合

在伺服器端,Socket.io 伺服器會初始化並附加至 HTTP 伺服器。與用戶端類似,Socket.io 伺服器與用戶端建立連線後,系統會為每個連線建立通訊端執行個體,可用於發出及監聽訊息。Socket.io 也提供介面,可建立「聊天室」或任意管道,讓 Socket 加入及離開。

// 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 也提供 Redis 轉接器,無論是哪個伺服器提供通訊端,都能將事件播送至所有用戶端。Socket.io 只會使用 Redis 的 Pub/Sub 機制,不會儲存任何資料。

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

Socket.io 的 Redis 轉接器可重複使用 Redis 用戶端,用於儲存聊天室的訊息記錄。每個容器都會建立與 Redis 執行個體的連線,而 Cloud Run 可以建立大量執行個體。這遠低於 Redis 可支援的 65,000 個連線。

重新連線

Cloud Run 的逾時時間上限為 60 分鐘。因此,您需要新增重新連線邏輯,以因應可能發生的逾時情況。在某些情況下,Socket.io 會在中斷連線或連線錯誤事件後,自動嘗試重新連線。我們無法保證用戶端會重新連線至同一個執行個體。

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

如果連線處於有效狀態,執行個體會持續存在,直到所有要求關閉或逾時為止。即使您使用 Cloud Run 工作階段相依性,新要求仍可能透過負載平衡機制分配至運作中的容器,讓容器縮減資源。如果擔心流量激增後仍有大量容器持續存在,可以降低逾時值上限,更頻繁地清除未使用的通訊端。

推出服務

  1. 建立 Memorystore for Redis 執行個體:

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

    更改下列內容:

    • INSTANCE_ID:執行個體的名稱,例如 my-redis-instance。
    • REGION_ID:所有資源和服務的區域,例如 europe-west1。

    系統會自動從預設服務網路範圍,為執行個體分配 IP 範圍。本教學課程使用 1 GB 的記憶體,在本機快取 Redis 執行個體中的訊息。進一步瞭解如何根據用途決定 Memorystore 執行個體的初始大小。

  2. 使用 Redis 執行個體授權網路的 IP 位址,定義環境變數:

     export REDISHOST=$(gcloud redis instances describe INSTANCE_ID --region REGION --format "value(host)")
  3. 建立服務帳戶,做為服務身分。根據預設,除了專案成員資格外,這項服務沒有其他權限。

    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
  4. 執行下列指令,找出 Redis 執行個體授權的虛擬私有雲網路名稱:

      gcloud redis instances describe INSTANCE_ID --region REGION --format "value(authorizedNetwork)"
    

    更改下列內容:

    • INSTANCE_ID:執行個體的名稱,例如 my-redis-instance。
    • REGION_ID:所有資源和服務的區域,例如 europe-west1。

    記下虛擬私有雲網路名稱。

  5. 建構容器映像檔並部署至 Cloud Run:

    gcloud run deploy chat-app --source . \
        --allow-unauthenticated \
        --timeout 3600 \
        --service-account chat-identity \
        --network NETWORK \
        --subnet SUBNET \
        --update-env-vars REDISHOST=$REDISHOST

    更改下列內容:

    • NETWORK 是 Redis 執行個體附加的授權虛擬私有雲網路名稱。
    • SUBNET 是子網路名稱。子網路不得小於 /26。直連虛擬私有雲輸出支援 IPv4 範圍 RFC 1918、RFC 6598 和 E 類。

    如果系統提示您安裝必要 API,請回覆 y。每個專案只需執行一次這項操作。如果您尚未按照設定頁面所述設定預設值,請提供平台和區域,回應其他提示。進一步瞭解如何從原始碼部署。

試用服務

如要試用完整服務,請按照下列步驟操作:

  1. 在瀏覽器中前往部署步驟提供的網址。

  2. 新增名稱和聊天室即可登入。

  3. 傳送訊息到聊天室!

如果您選擇繼續開發這些服務,請注意,這些服務對 Google Cloud 其餘部分的 Identity and Access Management (IAM) 存取權有限,必須授予其他 IAM 角色,才能存取許多其他服務。

清除所用資源

為避免系統向您的 Google Cloud 帳戶收取額外費用,請刪除您在本教學課程中部署的所有資源。

刪除專案

如果您是為了這個教學課程建立新專案,請刪除專案。如果您使用現有專案,並想保留專案,但不要本教學課程新增的變更,請刪除為本教學課程建立的資源。

如要避免付費,最簡單的方法就是刪除您為了本教學課程所建立的專案。

刪除專案的方法如下:

  1. 前往 Google Cloud 控制台的「Manage resources」(管理資源) 頁面。

    前往「Manage resources」(管理資源)

  2. 在專案清單中選取要刪除的專案,然後點選「Delete」(刪除)。
  3. 在對話方塊中輸入專案 ID,然後按一下 [Shut down] (關閉) 以刪除專案。

刪除教學課程資源

  1. 刪除您在本教學課程中部署的 Cloud Run 服務。Cloud Run 服務在收到要求前不會產生費用。

    如要刪除 Cloud Run 服務,請執行下列指令:

    gcloud run services delete SERVICE-NAME

    將 SERVICE-NAME 改為您的服務名稱。

    您也可以從Google Cloud 控制台刪除 Cloud Run 服務。

  2. 移除您在教學課程設定期間新增的 gcloud 預設區域設定:

     gcloud config unset run/region
    
  3. 移除專案設定:

     gcloud config unset project
    
  4. 刪除在本教學課程中建立的其他 Google Cloud 資源:

後續步驟