04-3. WebSocket 實戰應用
從聊天室到協作編輯,實作完整的即時通訊系統
目錄
🚀 WebSocket 實戰應用
🎯 實戰專案概覽
本篇將實作四個完整的 WebSocket 應用:
- 即時聊天室 💬
- 即時通知系統 🔔
- 協作編輯器 ✏️
- 多人遊戲同步 🎮
💬 專案一:即時聊天室
需求分析
功能:
✅ 多個使用者同時聊天
✅ 顯示線上人數
✅ 顯示「正在輸入...」
✅ 訊息歷史記錄
✅ 私訊功能後端實作(Node.js + ws)
// server.js
const WebSocket = require('ws');
const http = require('http');
const express = require('express');
const app = express();
const server = http.createServer(app);
const wss = new WebSocket.Server({ server });
// 儲存所有連線
const clients = new Map();
// 訊息歷史(實務應該用資料庫)
const messageHistory = [];
wss.on('connection', (ws) => {
let userId = null;
console.log('新使用者連線');
ws.on('message', (message) => {
const data = JSON.parse(message);
switch (data.type) {
case 'join':
handleJoin(ws, data);
break;
case 'message':
handleMessage(ws, data);
break;
case 'typing':
handleTyping(ws, data);
break;
case 'private':
handlePrivateMessage(ws, data);
break;
}
});
ws.on('close', () => {
handleDisconnect(ws);
});
// 加入聊天室
function handleJoin(ws, data) {
userId = data.userId;
const username = data.username;
clients.set(userId, {
ws: ws,
username: username
});
// 發送歷史訊息給新使用者
ws.send(JSON.stringify({
type: 'history',
messages: messageHistory
}));
// 廣播使用者加入
broadcast({
type: 'user_joined',
userId: userId,
username: username,
onlineCount: clients.size
});
console.log(`${username} 加入聊天室`);
}
// 處理聊天訊息
function handleMessage(ws, data) {
const message = {
type: 'message',
userId: userId,
username: clients.get(userId).username,
content: data.content,
timestamp: new Date().toISOString()
};
// 儲存訊息歷史
messageHistory.push(message);
if (messageHistory.length > 100) {
messageHistory.shift(); // 只保留最新 100 則
}
// 廣播給所有人
broadcast(message);
}
// 處理「正在輸入」
function handleTyping(ws, data) {
broadcast({
type: 'typing',
userId: userId,
username: clients.get(userId).username,
isTyping: data.isTyping
}, userId); // 排除自己
}
// 私訊
function handlePrivateMessage(ws, data) {
const targetClient = clients.get(data.targetUserId);
if (targetClient) {
const message = {
type: 'private',
fromUserId: userId,
fromUsername: clients.get(userId).username,
content: data.content,
timestamp: new Date().toISOString()
};
// 發送給目標使用者
targetClient.ws.send(JSON.stringify(message));
// 也發送給自己(顯示已發送)
ws.send(JSON.stringify({
...message,
type: 'private_sent'
}));
}
}
// 使用者離線
function handleDisconnect(ws) {
if (userId && clients.has(userId)) {
const username = clients.get(userId).username;
clients.delete(userId);
broadcast({
type: 'user_left',
userId: userId,
username: username,
onlineCount: clients.size
});
console.log(`${username} 離開聊天室`);
}
}
// 廣播訊息
function broadcast(message, excludeUserId = null) {
const messageStr = JSON.stringify(message);
clients.forEach((client, id) => {
if (id !== excludeUserId && client.ws.readyState === WebSocket.OPEN) {
client.ws.send(messageStr);
}
});
}
});
// 靜態檔案
app.use(express.static('public'));
server.listen(3000, () => {
console.log('聊天室伺服器啟動於 http://localhost:3000');
});前端實作(HTML + JavaScript)
<!-- public/index.html -->
<!DOCTYPE html>
<html>
<head>
<title>即時聊天室</title>
<style>
body {
font-family: Arial, sans-serif;
max-width: 800px;
margin: 0 auto;
padding: 20px;
}
#chat-box {
border: 1px solid #ccc;
height: 400px;
overflow-y: scroll;
padding: 10px;
margin-bottom: 10px;
background-color: #f9f9f9;
}
.message {
margin-bottom: 10px;
padding: 8px;
border-radius: 5px;
}
.message.self {
background-color: #d1e7dd;
text-align: right;
}
.message.other {
background-color: #fff;
}
.message.system {
background-color: #fff3cd;
text-align: center;
font-style: italic;
}
.typing-indicator {
color: #666;
font-style: italic;
font-size: 0.9em;
}
#input-area {
display: flex;
gap: 10px;
}
#message-input {
flex: 1;
padding: 10px;
border: 1px solid #ccc;
border-radius: 5px;
}
#send-btn {
padding: 10px 20px;
background-color: #007bff;
color: white;
border: none;
border-radius: 5px;
cursor: pointer;
}
#send-btn:hover {
background-color: #0056b3;
}
#online-count {
color: #28a745;
font-weight: bold;
}
</style>
</head>
<body>
<h1>即時聊天室 💬</h1>
<p>線上人數:<span id="online-count">0</span></p>
<div id="chat-box"></div>
<div class="typing-indicator" id="typing-indicator"></div>
<div id="input-area">
<input type="text" id="message-input" placeholder="輸入訊息..." />
<button id="send-btn">發送</button>
</div>
<script>
const chatBox = document.getElementById('chat-box');
const messageInput = document.getElementById('message-input');
const sendBtn = document.getElementById('send-btn');
const onlineCount = document.getElementById('online-count');
const typingIndicator = document.getElementById('typing-indicator');
// 生成隨機使用者 ID 和名稱
const userId = 'user_' + Math.random().toString(36).substr(2, 9);
const username = 'User_' + Math.floor(Math.random() * 1000);
// 建立 WebSocket 連線
const ws = new WebSocket('ws://localhost:3000');
ws.onopen = () => {
console.log('已連線到聊天室');
// 加入聊天室
ws.send(JSON.stringify({
type: 'join',
userId: userId,
username: username
}));
};
ws.onmessage = (event) => {
const data = JSON.parse(event.data);
switch (data.type) {
case 'history':
// 顯示歷史訊息
data.messages.forEach(msg => displayMessage(msg));
break;
case 'message':
displayMessage(data);
break;
case 'user_joined':
displaySystemMessage(`${data.username} 加入聊天室`);
updateOnlineCount(data.onlineCount);
break;
case 'user_left':
displaySystemMessage(`${data.username} 離開聊天室`);
updateOnlineCount(data.onlineCount);
break;
case 'typing':
showTypingIndicator(data);
break;
case 'private':
displayPrivateMessage(data, 'received');
break;
case 'private_sent':
displayPrivateMessage(data, 'sent');
break;
}
};
ws.onerror = (error) => {
console.error('WebSocket 錯誤:', error);
};
ws.onclose = () => {
console.log('連線已關閉');
displaySystemMessage('連線已中斷');
};
// 發送訊息
function sendMessage() {
const content = messageInput.value.trim();
if (content) {
ws.send(JSON.stringify({
type: 'message',
content: content
}));
messageInput.value = '';
stopTyping();
}
}
sendBtn.addEventListener('click', sendMessage);
messageInput.addEventListener('keypress', (e) => {
if (e.key === 'Enter') {
sendMessage();
}
});
// 「正在輸入」功能
let typingTimeout;
messageInput.addEventListener('input', () => {
if (messageInput.value.trim()) {
startTyping();
// 3 秒後自動停止
clearTimeout(typingTimeout);
typingTimeout = setTimeout(stopTyping, 3000);
} else {
stopTyping();
}
});
function startTyping() {
ws.send(JSON.stringify({
type: 'typing',
isTyping: true
}));
}
function stopTyping() {
ws.send(JSON.stringify({
type: 'typing',
isTyping: false
}));
}
// 顯示訊息
function displayMessage(data) {
const messageDiv = document.createElement('div');
messageDiv.className = 'message ' + (data.userId === userId ? 'self' : 'other');
const time = new Date(data.timestamp).toLocaleTimeString();
messageDiv.innerHTML = `
<strong>${data.username}</strong>
<span style="color: #999; font-size: 0.8em;">${time}</span>
<div>${escapeHtml(data.content)}</div>
`;
chatBox.appendChild(messageDiv);
chatBox.scrollTop = chatBox.scrollHeight;
}
// 顯示系統訊息
function displaySystemMessage(content) {
const messageDiv = document.createElement('div');
messageDiv.className = 'message system';
messageDiv.textContent = content;
chatBox.appendChild(messageDiv);
chatBox.scrollTop = chatBox.scrollHeight;
}
// 顯示私訊
function displayPrivateMessage(data, direction) {
const messageDiv = document.createElement('div');
messageDiv.className = 'message ' + (direction === 'sent' ? 'self' : 'other');
const prefix = direction === 'sent' ? '私訊給' : '來自';
messageDiv.innerHTML = `
<strong>${prefix} ${data.fromUsername}</strong>
<div>${escapeHtml(data.content)}</div>
`;
chatBox.appendChild(messageDiv);
chatBox.scrollTop = chatBox.scrollHeight;
}
// 更新線上人數
function updateOnlineCount(count) {
onlineCount.textContent = count;
}
// 顯示「正在輸入」
const typingUsers = new Set();
function showTypingIndicator(data) {
if (data.isTyping) {
typingUsers.add(data.username);
} else {
typingUsers.delete(data.username);
}
if (typingUsers.size > 0) {
const users = Array.from(typingUsers).join(', ');
typingIndicator.textContent = `${users} 正在輸入...`;
} else {
typingIndicator.textContent = '';
}
}
// HTML 跳脫(防 XSS)
function escapeHtml(text) {
const div = document.createElement('div');
div.textContent = text;
return div.innerHTML;
}
</script>
</body>
</html>🔔 專案二:即時通知系統
後端實作(Python + Flask-SocketIO)
# app.py
from flask import Flask, render_template
from flask_socketio import SocketIO, emit, join_room, leave_room
import time
from threading import Thread
app = Flask(__name__)
app.config['SECRET_KEY'] = 'secret!'
socketio = SocketIO(app, cors_allowed_origins="*")
# 使用者訂閱的通知類型
user_subscriptions = {}
@socketio.on('connect')
def handle_connect():
print(f'使用者連線:{request.sid}')
emit('connected', {'message': '已連線到通知系統'})
@socketio.on('subscribe')
def handle_subscribe(data):
"""訂閱通知類型"""
user_id = data['userId']
notification_types = data['types'] # ['order', 'message', 'system']
user_subscriptions[user_id] = {
'sid': request.sid,
'types': notification_types
}
# 加入對應的房間
for ntype in notification_types:
join_room(ntype)
emit('subscribed', {'types': notification_types})
print(f'{user_id} 訂閱:{notification_types}')
@socketio.on('unsubscribe')
def handle_unsubscribe(data):
"""取消訂閱"""
notification_type = data['type']
leave_room(notification_type)
emit('unsubscribed', {'type': notification_type})
@socketio.on('disconnect')
def handle_disconnect():
print(f'使用者離線:{request.sid}')
# 清理訂閱
for user_id, sub in list(user_subscriptions.items()):
if sub['sid'] == request.sid:
del user_subscriptions[user_id]
break
# 發送通知的 API
@app.route('/api/notify/<notification_type>/<user_id>', methods=['POST'])
def send_notification(notification_type, user_id):
"""發送通知給特定使用者"""
from flask import request as flask_request
data = flask_request.json
if user_id in user_subscriptions:
sub = user_subscriptions[user_id]
if notification_type in sub['types']:
socketio.emit('notification', {
'type': notification_type,
'title': data.get('title'),
'message': data.get('message'),
'timestamp': time.time()
}, room=sub['sid'])
return {'status': 'sent'}, 200
return {'status': 'user not subscribed'}, 404
# 廣播通知
@app.route('/api/broadcast/<notification_type>', methods=['POST'])
def broadcast_notification(notification_type):
"""廣播通知給所有訂閱者"""
from flask import request as flask_request
data = flask_request.json
socketio.emit('notification', {
'type': notification_type,
'title': data.get('title'),
'message': data.get('message'),
'timestamp': time.time()
}, room=notification_type)
return {'status': 'broadcasted'}, 200
# 模擬定期發送系統通知
def send_periodic_notifications():
while True:
time.sleep(30) # 每 30 秒
socketio.emit('notification', {
'type': 'system',
'title': '系統通知',
'message': '這是定期系統通知',
'timestamp': time.time()
}, room='system')
# 啟動背景執行緒
Thread(target=send_periodic_notifications, daemon=True).start()
if __name__ == '__main__':
socketio.run(app, debug=True, port=5000)前端實作
<!-- templates/notifications.html -->
<!DOCTYPE html>
<html>
<head>
<title>即時通知系統</title>
<script src="https://cdn.socket.io/4.5.4/socket.io.min.js"></script>
<style>
#notifications {
position: fixed;
top: 20px;
right: 20px;
width: 300px;
}
.notification {
background-color: #fff;
border-left: 4px solid #007bff;
padding: 15px;
margin-bottom: 10px;
box-shadow: 0 2px 5px rgba(0,0,0,0.1);
border-radius: 5px;
animation: slideIn 0.3s ease;
}
.notification.order { border-left-color: #28a745; }
.notification.message { border-left-color: #17a2b8; }
.notification.system { border-left-color: #ffc107; }
@keyframes slideIn {
from {
transform: translateX(100%);
opacity: 0;
}
to {
transform: translateX(0);
opacity: 1;
}
}
.notification-title {
font-weight: bold;
margin-bottom: 5px;
}
.notification-close {
float: right;
cursor: pointer;
color: #999;
}
</style>
</head>
<body>
<h1>即時通知系統 🔔</h1>
<h3>訂閱通知類型:</h3>
<label>
<input type="checkbox" id="sub-order" value="order" checked />
訂單通知
</label>
<label>
<input type="checkbox" id="sub-message" value="message" checked />
訊息通知
</label>
<label>
<input type="checkbox" id="sub-system" value="system" checked />
系統通知
</label>
<button id="update-subscription">更新訂閱</button>
<div id="notifications"></div>
<script>
const userId = 'user_' + Math.random().toString(36).substr(2, 9);
const socket = io('http://localhost:5000');
socket.on('connect', () => {
console.log('已連線');
// 訂閱通知
updateSubscription();
});
socket.on('notification', (data) => {
showNotification(data);
});
// 更新訂閱
document.getElementById('update-subscription').addEventListener('click', updateSubscription);
function updateSubscription() {
const types = [];
if (document.getElementById('sub-order').checked) types.push('order');
if (document.getElementById('sub-message').checked) types.push('message');
if (document.getElementById('sub-system').checked) types.push('system');
socket.emit('subscribe', {
userId: userId,
types: types
});
console.log('已訂閱:', types);
}
// 顯示通知
function showNotification(data) {
const container = document.getElementById('notifications');
const notification = document.createElement('div');
notification.className = `notification ${data.type}`;
notification.innerHTML = `
<span class="notification-close" onclick="this.parentElement.remove()">✖</span>
<div class="notification-title">${data.title}</div>
<div>${data.message}</div>
<div style="color: #999; font-size: 0.8em; margin-top: 5px;">
${new Date(data.timestamp * 1000).toLocaleTimeString()}
</div>
`;
container.insertBefore(notification, container.firstChild);
// 5 秒後自動關閉
setTimeout(() => {
notification.style.opacity = '0';
setTimeout(() => notification.remove(), 300);
}, 5000);
// 播放聲音(可選)
playNotificationSound();
}
function playNotificationSound() {
// 使用 Web Audio API 播放提示音
const audioContext = new (window.AudioContext || window.webkitAudioContext)();
const oscillator = audioContext.createOscillator();
const gainNode = audioContext.createGain();
oscillator.connect(gainNode);
gainNode.connect(audioContext.destination);
oscillator.frequency.value = 800;
oscillator.type = 'sine';
gainNode.gain.setValueAtTime(0.3, audioContext.currentTime);
gainNode.gain.exponentialRampToValueAtTime(0.01, audioContext.currentTime + 0.1);
oscillator.start(audioContext.currentTime);
oscillator.stop(audioContext.currentTime + 0.1);
}
</script>
</body>
</html>觸發通知(測試)
# 發送訂單通知給特定使用者
curl -X POST http://localhost:5000/api/notify/order/user_abc \
-H "Content-Type: application/json" \
-d '{"title": "訂單已出貨", "message": "您的訂單 #12345 已出貨"}'
# 廣播系統通知
curl -X POST http://localhost:5000/api/broadcast/system \
-H "Content-Type: application/json" \
-d '{"title": "系統維護", "message": "系統將於今晚 22:00 維護"}'✏️ 專案三:協作編輯器(簡化版 Google Docs)
後端實作(Node.js + Operational Transform)
// collaborative-server.js
const WebSocket = require('ws');
const wss = new WebSocket.Server({ port: 8080 });
// 儲存文件內容
let documentContent = '';
// 儲存操作歷史(簡化版)
const operationHistory = [];
wss.on('connection', (ws) => {
console.log('新使用者連線');
// 發送當前文件內容
ws.send(JSON.stringify({
type: 'init',
content: documentContent
}));
ws.on('message', (message) => {
const data = JSON.parse(message);
if (data.type === 'edit') {
handleEdit(ws, data);
}
});
});
function handleEdit(ws, data) {
const { operation, content } = data;
// 應用操作到文件
documentContent = content;
// 記錄操作
operationHistory.push({
operation: operation,
timestamp: Date.now()
});
// 廣播給其他使用者
wss.clients.forEach((client) => {
if (client !== ws && client.readyState === WebSocket.OPEN) {
client.send(JSON.stringify({
type: 'update',
operation: operation,
content: content
}));
}
});
}
console.log('協作編輯伺服器啟動於 ws://localhost:8080');前端實作
<!DOCTYPE html>
<html>
<head>
<title>協作編輯器</title>
<style>
#editor {
width: 100%;
height: 400px;
padding: 10px;
font-family: monospace;
font-size: 14px;
border: 1px solid #ccc;
}
#status {
color: #28a745;
margin: 10px 0;
}
</style>
</head>
<body>
<h1>協作編輯器 ✏️</h1>
<div id="status">連線中...</div>
<textarea id="editor" placeholder="開始輸入..."></textarea>
<script>
const editor = document.getElementById('editor');
const status = document.getElementById('status');
const ws = new WebSocket('ws://localhost:8080');
let isUpdating = false;
ws.onopen = () => {
status.textContent = '✅ 已連線';
};
ws.onmessage = (event) => {
const data = JSON.parse(event.data);
if (data.type === 'init') {
// 初始化文件內容
editor.value = data.content;
} else if (data.type === 'update') {
// 其他使用者的更新
isUpdating = true;
// 儲存游標位置
const cursorPosition = editor.selectionStart;
editor.value = data.content;
// 恢復游標位置
editor.setSelectionRange(cursorPosition, cursorPosition);
isUpdating = false;
}
};
// 監聽輸入
let updateTimeout;
editor.addEventListener('input', () => {
if (isUpdating) return;
// 防抖:500ms 後才發送
clearTimeout(updateTimeout);
updateTimeout = setTimeout(() => {
ws.send(JSON.stringify({
type: 'edit',
operation: {
// 簡化版,實際應該用 Operational Transform
type: 'replace',
content: editor.value
},
content: editor.value
}));
}, 500);
});
ws.onerror = (error) => {
status.textContent = '❌ 連線錯誤';
console.error(error);
};
ws.onclose = () => {
status.textContent = '⚠️ 連線已中斷';
};
</script>
</body>
</html>🎮 專案四:多人遊戲同步(簡單的多人移動遊戲)
完整實作
// game-server.js
const WebSocket = require('ws');
const wss = new WebSocket.Server({ port: 9000 });
// 遊戲狀態
const players = new Map();
// 遊戲循環(60 FPS)
setInterval(() => {
broadcastGameState();
}, 1000 / 60);
wss.on('connection', (ws) => {
const playerId = generateId();
// 新玩家加入
players.set(playerId, {
ws: ws,
x: Math.random() * 800,
y: Math.random() * 600,
color: randomColor()
});
// 發送玩家 ID
ws.send(JSON.stringify({
type: 'init',
playerId: playerId,
players: serializePlayers()
}));
// 廣播新玩家加入
broadcast({
type: 'player_joined',
playerId: playerId,
player: players.get(playerId)
}, playerId);
ws.on('message', (message) => {
const data = JSON.parse(message);
if (data.type === 'move') {
handleMove(playerId, data);
}
});
ws.on('close', () => {
players.delete(playerId);
broadcast({
type: 'player_left',
playerId: playerId
});
});
});
function handleMove(playerId, data) {
const player = players.get(playerId);
if (player) {
player.x = data.x;
player.y = data.y;
}
}
function broadcastGameState() {
const gameState = {
type: 'game_state',
players: serializePlayers()
};
const message = JSON.stringify(gameState);
players.forEach((player) => {
if (player.ws.readyState === WebSocket.OPEN) {
player.ws.send(message);
}
});
}
function broadcast(message, excludeId = null) {
const messageStr = JSON.stringify(message);
players.forEach((player, id) => {
if (id !== excludeId && player.ws.readyState === WebSocket.OPEN) {
player.ws.send(messageStr);
}
});
}
function serializePlayers() {
const result = {};
players.forEach((player, id) => {
result[id] = {
x: player.x,
y: player.y,
color: player.color
};
});
return result;
}
function generateId() {
return 'player_' + Math.random().toString(36).substr(2, 9);
}
function randomColor() {
return '#' + Math.floor(Math.random() * 16777215).toString(16);
}
console.log('遊戲伺服器啟動於 ws://localhost:9000');<!-- game-client.html -->
<!DOCTYPE html>
<html>
<head>
<title>多人遊戲</title>
<style>
body {
margin: 0;
overflow: hidden;
background-color: #f0f0f0;
}
canvas {
display: block;
border: 2px solid #333;
}
#info {
position: absolute;
top: 10px;
left: 10px;
font-family: Arial, sans-serif;
background-color: rgba(255, 255, 255, 0.8);
padding: 10px;
border-radius: 5px;
}
</style>
</head>
<body>
<div id="info">
玩家數:<span id="player-count">0</span><br>
使用方向鍵移動
</div>
<canvas id="game" width="800" height="600"></canvas>
<script>
const canvas = document.getElementById('game');
const ctx = canvas.getContext('2d');
const playerCountEl = document.getElementById('player-count');
const ws = new WebSocket('ws://localhost:9000');
let myPlayerId = null;
let players = {};
// 本地玩家位置(客戶端預測)
let myPosition = { x: 0, y: 0 };
const speed = 5;
// 鍵盤狀態
const keys = {};
ws.onmessage = (event) => {
const data = JSON.parse(event.data);
switch (data.type) {
case 'init':
myPlayerId = data.playerId;
players = data.players;
myPosition = players[myPlayerId];
break;
case 'player_joined':
players[data.playerId] = data.player;
break;
case 'player_left':
delete players[data.playerId];
break;
case 'game_state':
// 更新其他玩家位置
Object.keys(data.players).forEach((id) => {
if (id !== myPlayerId) {
players[id] = data.players[id];
}
});
break;
}
playerCountEl.textContent = Object.keys(players).length;
};
// 鍵盤事件
document.addEventListener('keydown', (e) => {
keys[e.key] = true;
});
document.addEventListener('keyup', (e) => {
keys[e.key] = false;
});
// 遊戲循環
function gameLoop() {
// 處理移動(客戶端預測)
let moved = false;
if (keys['ArrowUp'] && myPosition.y > 0) {
myPosition.y -= speed;
moved = true;
}
if (keys['ArrowDown'] && myPosition.y < canvas.height) {
myPosition.y += speed;
moved = true;
}
if (keys['ArrowLeft'] && myPosition.x > 0) {
myPosition.x -= speed;
moved = true;
}
if (keys['ArrowRight'] && myPosition.x < canvas.width) {
myPosition.x += speed;
moved = true;
}
// 發送位置更新
if (moved) {
ws.send(JSON.stringify({
type: 'move',
x: myPosition.x,
y: myPosition.y
}));
// 更新本地狀態
if (players[myPlayerId]) {
players[myPlayerId].x = myPosition.x;
players[myPlayerId].y = myPosition.y;
}
}
// 渲染
render();
requestAnimationFrame(gameLoop);
}
function render() {
// 清空畫布
ctx.fillStyle = '#f0f0f0';
ctx.fillRect(0, 0, canvas.width, canvas.height);
// 繪製所有玩家
Object.keys(players).forEach((id) => {
const player = players[id];
ctx.fillStyle = player.color;
ctx.beginPath();
ctx.arc(player.x, player.y, 20, 0, Math.PI * 2);
ctx.fill();
// 標示自己
if (id === myPlayerId) {
ctx.strokeStyle = '#000';
ctx.lineWidth = 3;
ctx.stroke();
}
// 顯示 ID(簡化)
ctx.fillStyle = '#000';
ctx.font = '12px Arial';
ctx.textAlign = 'center';
ctx.fillText(id.substr(0, 8), player.x, player.y - 30);
});
}
// 啟動遊戲循環
requestAnimationFrame(gameLoop);
</script>
</body>
</html>📝 總結
我們實作了四個完整的 WebSocket 應用:
即時聊天室 💬
- 多使用者即時通訊
- 線上狀態、正在輸入
- 私訊功能
即時通知系統 🔔
- 訂閱/取消訂閱
- 分類通知
- 視覺化通知彈窗
協作編輯器 ✏️
- 多人同步編輯
- 即時更新
- 游標位置保持
多人遊戲 🎮
- 即時位置同步
- 客戶端預測
- 60 FPS 更新
核心技術:
- WebSocket 雙向通訊
- 廣播/房間機制
- 客戶端預測
- 斷線重連
- 狀態同步
這些範例展示了 WebSocket 在實際應用中的強大能力!
🔗 延伸閱讀
- 上一篇:04-2. WebSocket vs HTTP
- 下一章:05-1. 即時通訊協定概覽
- Socket.IO 文件:https://socket.io/
- WebSocket API 規範:https://websockets.spec.whatwg.org/