傳統的 REST API 屬於請求-回應模式,無法即時主動推送訊息。但在會員系統中,聊天室、即時提醒(如新公告、好友上線通知)都需要即時互動能力。利用 API Gateway WebSocket,可以讓後端主動推播事件給前端,帶來更流暢的使用者體驗。
這個 Lab 解決的痛點是「即時性不足」。若僅依賴輪詢(Polling),會增加成本與延遲;而 WebSocket 提供長連線方式,讓系統能立即廣播通知或訊息。在 Serverless 架構中,API Gateway WebSocket 搭配 Lambda 與 DynamoDB,可打造一個無伺服器聊天室與提醒系統,擁有高擴展性與低維運成本。
roomId
區分訊息群組。無
Connections
,主鍵為 connectionId
💡由於同樣的user可能會透過不同裝置有不同的「Connection ID」,所以建議會用主鍵為「Connection ID」的DynamoDB來作為連線的依據。
進入「DynamoDB」頁面。
創建一個新的資料表。
設定名稱及主鍵。
進入「IAM」頁面。
創建一個新的IAM Role。
選擇用途為「AWS服務」。(稍後需要用於3個Lambda)
選擇政策「AWSLambdaBasicExecutionRole」,並點選「下一步」。
設定角色名稱,並點選創建。
重新進到該IAM Role中。
添加「許可政策」。
添加以下權限。
dynamodb:PutItem
dynamodb:DeleteItem
dynamodb:Scan
execute-api:ManageConnections
點選下一步。
命名政策。
完成畫面。
進入「Lambda」頁面。
創建3個新的函數。
輸入函數名稱,並選擇編撰語言。名稱分別為:OnConnect、OnDisconnect、OnMessage
跳過建議畫面。
寫入程式碼、部署,並新增環境變數。
(1) 程式碼 - OnConnect
程式碼範例-OnConnect
// index.mjs (或 index.js,取決於您的 Lambda 執行模式設定)
import AWS from 'aws-sdk';
const DYNAMODB = new AWS.DynamoDB.DocumentClient();
const TABLE_NAME = process.env.TABLE_NAME;
export const handler = async (event) => {
// 從連線事件中取得連線 ID
const connectionId = event.requestContext.connectionId;
try {
await DYNAMODB.put({
TableName: TABLE_NAME,
Item: {
connectionId: connectionId,
// 可選:設定 TTL (Time-To-Live) 欄位來自動清理過期連線
}
}).promise();
return { statusCode: 200, body: 'Connected.' };
} catch (err) {
console.error('Connection error:', err);
return { statusCode: 500, body: 'Failed to connect.' };
}
};
環境變數:「TABLE_NAME:DynamoDB 資料表名稱」。
(2) 程式碼 - OnDisconnec
程式碼範例-OnDisconnec
// index.mjs (OnDisconnectLambda - 統一使用 AWS SDK v3)
// 1. 匯入必要的 SDK v3 模組
import { DynamoDBClient } from "@aws-sdk/client-dynamodb";
import { DynamoDBDocumentClient, DeleteCommand } from "@aws-sdk/lib-dynamodb";
const TABLE_NAME = process.env.TABLE_NAME;
// 2. 初始化客戶端 (在 Handler 外部初始化以重複使用,降低冷啟動時間)
const client = new DynamoDBClient({});
// 使用 DocumentClient 讓操作更容易,處理 JavaScript 物件
const DYNAMODB = DynamoDBDocumentClient.from(client);
export const handler = async (event) => {
// 從斷線事件中取得連線 ID
const connectionId = event.requestContext.connectionId;
try {
// 3. 使用 v3 的 DeleteCommand 進行刪除操作
await DYNAMODB.send(new DeleteCommand({
TableName: TABLE_NAME,
Key: { connectionId: connectionId } // v3 DocumentClient 會自動處理 Key 類型
}));
return { statusCode: 200, body: 'Disconnected.' };
} catch (err) {
console.error('Disconnection error:', err);
return { statusCode: 500, body: 'Failed to disconnect.' };
}
};
環境變數:「TABLE_NAME:DynamoDB 資料表名稱」
(3) 程式碼 - OnMessage
程式碼範例-OnMessage
// index.mjs (使用 AWS SDK v3)
// 1. 匯入必要的模組
import { DynamoDBClient } from "@aws-sdk/client-dynamodb";
// 引入 ScanCommand 和 DeleteCommand
import { DynamoDBDocumentClient, DeleteCommand, ScanCommand } from "@aws-sdk/lib-dynamodb";
// 引入 v3 的 APIGW 模組
import { ApiGatewayManagementApiClient, PostToConnectionCommand } from "@aws-sdk/client-apigatewaymanagementapi";
const TABLE_NAME = process.env.TABLE_NAME;
const WS_ENDPOINT = process.env.WS_ENDPOINT;
// 2. 初始化客戶端
// DynamoDB Document Client
const dbClient = new DynamoDBClient({});
const DYNAMODB = DynamoDBDocumentClient.from(dbClient);
// API Gateway Management Client (Endpoint 必須在初始化時傳入)
const APIGW_MANAGEMENT = new ApiGatewayManagementApiClient({
endpoint: WS_ENDPOINT
});
/**
* 嘗試向單個連線推送數據。
* 如果連線過期 (410),則觸發背景清理。
* 無論推播或清理失敗,此函數都不會拋出錯誤 (throw error),以確保整體廣播的健壯性。
* @param {string} connectionId
* @param {any} data
*/
const sendToOne = async (connectionId, data) => {
try {
// 使用 v3 的 PostToConnectionCommand
await APIGW_MANAGEMENT.send(new PostToConnectionCommand({
ConnectionId: connectionId,
Data: data,
}));
} catch (error) {
// 處理連線可能已過期 (410 Gone)
if (error.name === 'GoneException' || error.statusCode === 410) {
console.log(`Found stale connection: ${connectionId}`);
// 💡 關鍵修正 1: 移除 await,讓清理作業在背景執行 (避免超時)
// 💡 關鍵修正 2: 使用 .catch() 捕獲並忽略清理過程中的任何錯誤 (如 AccessDeniedException)
DYNAMODB.send(new DeleteCommand({
TableName: TABLE_NAME,
Key: { connectionId: connectionId }
})).catch(dbError => {
// 記錄錯誤,但不將其向上拋出,確保 Promise 不會被拒絕
console.error(`ERROR: Failed to delete stale connection ${connectionId}:`, dbError);
});
// 這裡不拋出錯誤,讓 Promise 被視為已解決 (Resolved)
} else {
// 處理非 410 的其他錯誤 (如權限不足、連線池問題等)
console.error(`Error sending message (non-410) to ${connectionId}:`, error);
// 🚨 關鍵修正 3: 移除 throw error。
// 確保單個推播失敗不會導致 Promise.allSettled 失敗,
// 最終使 handler 成功返回 200,解決前端顯示 500 錯誤的問題。
}
}
// sendToOne 函式現在保證不會拋出錯誤,讓上層的 Promise.allSettled 順利完成
};
export const handler = async (event) => {
try {
// 🚨 步驟 1: 新增檢查,確保 body 存在 🚨
if (!event.body) {
console.log("Received a non-body event or empty message. Ignoring.");
// 確保返回 200
return { statusCode: 200, body: 'Empty body ignored.' };
}
// 2. 解析傳入的訊息
const body = JSON.parse(event.body);
// 💡 直接使用純文字內容作為廣播訊息
const messageString = body.message || 'No message provided';
// 3. 掃描所有連線 ID (使用 v3 的 ScanCommand)
const scanResult = await DYNAMODB.send(new ScanCommand({
TableName: TABLE_NAME,
ProjectionExpression: 'connectionId'
}));
const connections = scanResult.Items;
// 4. 廣播訊息給所有連線
const postCalls = connections.map(conn =>
// 💡 將純文字字串傳遞給 sendToOne
sendToOne(conn.connectionId, messageString)
);
// 等待所有推播嘗試完成。由於 sendToOne 不拋出錯誤,這裡將只等待完成,而不是失敗。
await Promise.allSettled(postCalls);
return { statusCode: 200, body: 'Data sent.' };
} catch (err) {
// 只有 DynamoDB Scan 或 JSON.parse 等無法恢復的錯誤才會到達這裡
console.error('Broadcasting fatal error:', err);
return { statusCode: 500, body: 'Failed to process message.' };
}
};
環境變數:
TABLE_NAME:DynamoDB 資料表名稱
WS_ENDPOINT:部署 API Gateway WebSocket API 後所得到的 WebSocket URL(wss://{API_ID}.execute-api.{REGION}.amazonaws.com/dev)
三個Lambda分別的功能為:
進入「API Gateway」頁面。
創建一個新的API Gateway。
創建類型選擇WebSocket API。
設定名稱及路由選擇表達式「$request.body.action」。
設定三個路由路徑。
分別選擇剛剛創建的Lambda。
命名階段,並按下一步。
完成創建。
部署API。
輸入部署路徑,並確認部署。
安裝node
brew install node
查看node版本
node -v
下載wscat
npm install -g wscat
wscat
測試連線:# 替換為您部署階段的 WebSocket URL
wscat -c wss://{api-id}.execute-api.{region}.amazonaws.com/dev
開啟多個終端機:在兩個或多個終端機中執行上述 wscat
連線。
發送訊息:在其中一個終端機輸入訊息:
> {"message": "Hello Everyone! (Test Broadcast)"}
檢查所有連線:所有連線的終端機都應該即時收到 {"msg": "Broadcast: Hello Everyone! (Test Broadcast)"}
的推播訊息。
檢查 DynamoDB:會看到「回傳目前連線數量」,確認 Connections
表格中是否新增了連線 ID。當您關閉 wscat
時,連線 ID 應被移除。
今天的 Lab 展示了如何利用 API Gateway WebSocket 搭配 Lambda + DynamoDB,打造一個即時聊天與提醒系統。這不僅解決了 REST API 無法即時推送的痛點,也提供了可擴充的基礎,未來能延伸到通知系統、多人協作工具甚至遊戲對戰平台,讓 Serverless 架構更具互動性。