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

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

雖然您可以設定 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. 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.

    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. Enable the Cloud Run, Memorystore for Redis, Artifact Registry, and Cloud Build APIs.

    Roles required to enable APIs

    To enable APIs, you need the Service Usage Admin IAM role (roles/serviceusage.serviceUsageAdmin), which contains the serviceusage.services.enable permission. Learn how to grant roles.

    Enable the APIs

  7. 安裝並初始化 gcloud CLI
  8. 必要的角色

    如要取得完成本教學課程所需的權限,請要求管理員在專案中授予您下列 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 執行個體,可用於發出及監聽訊息。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 服務都沒問題。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 範圍。本教學課程會為 Redis 執行個體中的訊息本機快取使用 1 GB 的記憶體。進一步瞭解如何根據用途決定 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

    記下 VPC 網路名稱。

  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 執行個體所附加的授權 VPC 網路名稱。
    • SUBNET 是子網路的名稱。子網路不得小於 /26。直接虛擬私有雲輸出支援 IPv4 範圍 RFC 1918RFC 6598 和 E 類。

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

試用這項服務

如要試用完整服務:

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

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

  3. 傳送訊息至聊天室!

如果您選擇繼續開發這些服務,請注意,這些服務的其餘部分具有受限的身分與存取權管理 (IAM) 存取權,且需要額外指派 IAM 角色,才能存取許多其他服務。 Google Cloud

清除所用資源

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

刪除專案

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

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

如要刪除專案:

  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.

刪除教學課程資源

  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 資源:

後續步驟