WebSockets

WebSocket概述

WebSocket是HTML5提供的一种在单个TCP连接上进行全双工通信的协议。它允许服务器主动向客户端推送数据,实现了真正的实时通信。

东巴文(db-w.cn) 认为:WebSocket是现代实时Web应用的基石,让服务器推送成为可能。

WebSocket特点

核心特点

特点 说明
全双工通信 客户端和服务器可以同时发送和接收数据
实时性强 服务器可以主动推送数据,无需客户端轮询
开销小 建立连接后,数据帧头部开销很小
保持连接 连接保持打开状态,避免频繁建立连接
跨域支持 支持跨域通信

WebSocket vs HTTP

// HTTP: 请求-响应模式
// 客户端 -> 服务器: 请求
// 服务器 -> 客户端: 响应
// 每次请求都需要建立新的连接

// WebSocket: 全双工通信
// 客户端 <-> 服务器: 双向通信
// 连接保持打开,双方都可以主动发送数据

const comparison = {
    HTTP: {
        通信模式: '半双工',
        连接方式: '短连接',
        实时性: '差(需要轮询)',
        开销: '大(每次请求都有完整HTTP头)',
        适用场景: '传统Web应用'
    },
    WebSocket: {
        通信模式: '全双工',
        连接方式: '长连接',
        实时性: '好(服务器主动推送)',
        开销: '小(数据帧头部仅2-10字节)',
        适用场景: '实时应用、游戏、聊天'
    }
};

创建WebSocket连接

基本连接

<!DOCTYPE html>
<html lang="zh-CN">
<head>
    <meta charset="UTF-8">
    <meta name="viewport" content="width=device-width, initial-scale=1.0">
    <title>WebSocket基本连接</title>
    <style>
        body {
            font-family: Arial, sans-serif;
            max-width: 800px;
            margin: 50px auto;
            padding: 20px;
        }
        
        .status {
            padding: 10px;
            border-radius: 5px;
            margin-bottom: 20px;
        }
        
        .status.connected {
            background: #d4edda;
            color: #155724;
        }
        
        .status.disconnected {
            background: #f8d7da;
            color: #721c24;
        }
        
        .status.connecting {
            background: #fff3cd;
            color: #856404;
        }
        
        .messages {
            height: 400px;
            border: 1px solid #ddd;
            border-radius: 5px;
            padding: 10px;
            overflow-y: auto;
            background: #f9f9f9;
        }
        
        .message {
            padding: 8px;
            margin: 5px 0;
            border-radius: 5px;
        }
        
        .message.sent {
            background: #007bff;
            color: white;
            text-align: right;
        }
        
        .message.received {
            background: #e9ecef;
            text-align: left;
        }
        
        .message.system {
            background: #ffc107;
            text-align: center;
            font-weight: bold;
        }
        
        .input-area {
            display: flex;
            gap: 10px;
            margin-top: 20px;
        }
        
        input {
            flex: 1;
            padding: 10px;
            border: 1px solid #ddd;
            border-radius: 5px;
        }
        
        button {
            padding: 10px 20px;
            background: #007bff;
            color: white;
            border: none;
            border-radius: 5px;
            cursor: pointer;
        }
        
        button:hover {
            background: #0056b3;
        }
        
        button:disabled {
            background: #ccc;
            cursor: not-allowed;
        }
    </style>
</head>
<body>
    <h1>WebSocket基本连接</h1>
    
    <div class="status disconnected" id="status">
        未连接
    </div>
    
    <div class="messages" id="messages"></div>
    
    <div class="input-area">
        <input type="text" id="messageInput" placeholder="输入消息..." disabled>
        <button id="sendBtn" disabled>发送</button>
        <button id="connectBtn">连接</button>
        <button id="disconnectBtn" disabled>断开</button>
    </div>
    
    <script>
        let ws = null;
        const status = document.getElementById('status');
        const messages = document.getElementById('messages');
        const messageInput = document.getElementById('messageInput');
        const sendBtn = document.getElementById('sendBtn');
        const connectBtn = document.getElementById('connectBtn');
        const disconnectBtn = document.getElementById('disconnectBtn');
        
        // 连接WebSocket
        function connect() {
            updateStatus('connecting', '连接中...');
            
            // 创建WebSocket连接
            // 使用公共WebSocket测试服务器
            ws = new WebSocket('wss://echo.websocket.org');
            
            // 连接打开
            ws.addEventListener('open', function(event) {
                updateStatus('connected', '已连接');
                addMessage('系统', '连接成功!');
                
                messageInput.disabled = false;
                sendBtn.disabled = false;
                connectBtn.disabled = true;
                disconnectBtn.disabled = false;
            });
            
            // 接收消息
            ws.addEventListener('message', function(event) {
                addMessage('服务器', event.data);
            });
            
            // 连接关闭
            ws.addEventListener('close', function(event) {
                updateStatus('disconnected', '已断开');
                addMessage('系统', '连接已关闭');
                
                messageInput.disabled = true;
                sendBtn.disabled = true;
                connectBtn.disabled = false;
                disconnectBtn.disabled = true;
            });
            
            // 连接错误
            ws.addEventListener('error', function(event) {
                updateStatus('disconnected', '连接错误');
                addMessage('系统', '连接发生错误');
            });
        }
        
        // 断开连接
        function disconnect() {
            if (ws) {
                ws.close();
                ws = null;
            }
        }
        
        // 发送消息
        function sendMessage() {
            const message = messageInput.value.trim();
            if (message && ws && ws.readyState === WebSocket.OPEN) {
                ws.send(message);
                addMessage('我', message);
                messageInput.value = '';
            }
        }
        
        // 更新状态
        function updateStatus(state, text) {
            status.className = 'status ' + state;
            status.textContent = text;
        }
        
        // 添加消息
        function addMessage(sender, text) {
            const messageDiv = document.createElement('div');
            messageDiv.className = 'message';
            
            if (sender === '系统') {
                messageDiv.classList.add('system');
                messageDiv.textContent = text;
            } else if (sender === '我') {
                messageDiv.classList.add('sent');
                messageDiv.textContent = text;
            } else {
                messageDiv.classList.add('received');
                messageDiv.textContent = `${sender}: ${text}`;
            }
            
            messages.appendChild(messageDiv);
            messages.scrollTop = messages.scrollHeight;
        }
        
        // 事件绑定
        connectBtn.addEventListener('click', connect);
        disconnectBtn.addEventListener('click', disconnect);
        sendBtn.addEventListener('click', sendMessage);
        messageInput.addEventListener('keypress', function(e) {
            if (e.key === 'Enter') {
                sendMessage();
            }
        });
    </script>
</body>
</html>

WebSocket属性

readyState状态

// WebSocket连接状态
const WebSocketState = {
    CONNECTING: 0, // 正在连接
    OPEN: 1,       // 已连接
    CLOSING: 2,    // 正在关闭
    CLOSED: 3      // 已关闭
};

// 检查连接状态
function checkConnection(ws) {
    switch (ws.readyState) {
        case WebSocket.CONNECTING:
            console.log('正在连接...');
            break;
        case WebSocket.OPEN:
            console.log('连接已打开');
            break;
        case WebSocket.CLOSING:
            console.log('连接正在关闭...');
            break;
        case WebSocket.CLOSED:
            console.log('连接已关闭');
            break;
    }
}

// WebSocket其他属性
const wsProperties = {
    url: 'WebSocket服务器的URL',
    protocol: '服务器选择的子协议',
    bufferedAmount: '未发送到服务器的字节数',
    binaryType: '二进制数据类型(blob|arraybuffer)'
};

发送消息

发送文本和二进制数据

<!DOCTYPE html>
<html lang="zh-CN">
<head>
    <meta charset="UTF-8">
    <meta name="viewport" content="width=device-width, initial-scale=1.0">
    <title>WebSocket发送消息</title>
    <style>
        body {
            font-family: Arial, sans-serif;
            max-width: 800px;
            margin: 50px auto;
            padding: 20px;
        }
        
        .section {
            margin: 20px 0;
            padding: 20px;
            border: 1px solid #ddd;
            border-radius: 5px;
        }
        
        h3 {
            margin-top: 0;
        }
        
        textarea {
            width: 100%;
            height: 100px;
            padding: 10px;
            border: 1px solid #ddd;
            border-radius: 5px;
            font-family: monospace;
        }
        
        button {
            padding: 10px 20px;
            margin: 10px 5px 10px 0;
            background: #007bff;
            color: white;
            border: none;
            border-radius: 5px;
            cursor: pointer;
        }
        
        button:hover {
            background: #0056b3;
        }
        
        .log {
            background: #f9f9f9;
            padding: 10px;
            border-radius: 5px;
            max-height: 300px;
            overflow-y: auto;
            font-family: monospace;
            font-size: 14px;
        }
        
        .log-entry {
            padding: 5px 0;
            border-bottom: 1px solid #eee;
        }
        
        .log-entry.sent {
            color: #007bff;
        }
        
        .log-entry.received {
            color: #28a745;
        }
        
        .log-entry.error {
            color: #dc3545;
        }
    </style>
</head>
<body>
    <h1>WebSocket发送消息</h1>
    
    <div class="section">
        <h3>发送文本消息</h3>
        <textarea id="textInput" placeholder="输入文本消息..."></textarea>
        <button onclick="sendText()">发送文本</button>
    </div>
    
    <div class="section">
        <h3>发送JSON数据</h3>
        <textarea id="jsonInput">{
    "type": "message",
    "content": "Hello WebSocket!",
    "timestamp": "2024-01-01T00:00:00Z"
}</textarea>
        <button onclick="sendJSON()">发送JSON</button>
    </div>
    
    <div class="section">
        <h3>发送二进制数据</h3>
        <input type="file" id="fileInput">
        <button onclick="sendBinary()">发送文件</button>
        <button onclick="sendArrayBuffer()">发送ArrayBuffer</button>
    </div>
    
    <div class="section">
        <h3>消息日志</h3>
        <div class="log" id="log"></div>
    </div>
    
    <script>
        let ws = null;
        
        // 连接WebSocket
        function connect() {
            ws = new WebSocket('wss://echo.websocket.org');
            
            ws.binaryType = 'arraybuffer'; // 设置二进制类型
            
            ws.addEventListener('open', function() {
                addLog('系统', '连接成功');
            });
            
            ws.addEventListener('message', function(event) {
                if (event.data instanceof ArrayBuffer) {
                    addLog('接收', `二进制数据: ${event.data.byteLength} 字节`);
                } else {
                    addLog('接收', event.data);
                }
            });
            
            ws.addEventListener('error', function(error) {
                addLog('错误', '连接错误', 'error');
            });
            
            ws.addEventListener('close', function() {
                addLog('系统', '连接关闭');
            });
        }
        
        // 发送文本
        function sendText() {
            const text = document.getElementById('textInput').value;
            if (ws && ws.readyState === WebSocket.OPEN) {
                ws.send(text);
                addLog('发送', text, 'sent');
            }
        }
        
        // 发送JSON
        function sendJSON() {
            const json = document.getElementById('jsonInput').value;
            if (ws && ws.readyState === WebSocket.OPEN) {
                try {
                    const data = JSON.parse(json);
                    ws.send(JSON.stringify(data));
                    addLog('发送', JSON.stringify(data), 'sent');
                } catch (e) {
                    addLog('错误', 'JSON格式错误', 'error');
                }
            }
        }
        
        // 发送二进制文件
        function sendBinary() {
            const fileInput = document.getElementById('fileInput');
            const file = fileInput.files[0];
            
            if (file && ws && ws.readyState === WebSocket.OPEN) {
                const reader = new FileReader();
                reader.onload = function(e) {
                    ws.send(e.target.result);
                    addLog('发送', `文件: ${file.name} (${file.size} 字节)`, 'sent');
                };
                reader.readAsArrayBuffer(file);
            }
        }
        
        // 发送ArrayBuffer
        function sendArrayBuffer() {
            if (ws && ws.readyState === WebSocket.OPEN) {
                const buffer = new ArrayBuffer(10);
                const view = new Uint8Array(buffer);
                for (let i = 0; i < 10; i++) {
                    view[i] = i;
                }
                ws.send(buffer);
                addLog('发送', 'ArrayBuffer: [0, 1, 2, 3, 4, 5, 6, 7, 8, 9]', 'sent');
            }
        }
        
        // 添加日志
        function addLog(type, message, className = '') {
            const log = document.getElementById('log');
            const entry = document.createElement('div');
            entry.className = 'log-entry ' + className;
            entry.textContent = `[${new Date().toLocaleTimeString()}] ${type}: ${message}`;
            log.appendChild(entry);
            log.scrollTop = log.scrollHeight;
        }
        
        // 初始化连接
        connect();
    </script>
</body>
</html>

接收消息

处理不同类型消息

// WebSocket消息处理
ws.addEventListener('message', function(event) {
    // 文本消息
    if (typeof event.data === 'string') {
        console.log('文本消息:', event.data);
        
        // 尝试解析JSON
        try {
            const data = JSON.parse(event.data);
            console.log('JSON数据:', data);
        } catch (e) {
            // 不是JSON格式
        }
    }
    // 二进制消息
    else if (event.data instanceof Blob) {
        console.log('Blob数据:', event.data);
        
        // 转换为文本
        const reader = new FileReader();
        reader.onload = function(e) {
            console.log('Blob转文本:', e.target.result);
        };
        reader.readAsText(event.data);
    }
    // ArrayBuffer消息
    else if (event.data instanceof ArrayBuffer) {
        console.log('ArrayBuffer数据:', event.data);
        
        // 解析数据
        const view = new Uint8Array(event.data);
        console.log('字节数组:', view);
    }
});

连接状态管理

状态监控

class WebSocketManager {
    constructor(url) {
        this.url = url;
        this.ws = null;
        this.reconnectAttempts = 0;
        this.maxReconnectAttempts = 5;
        this.reconnectDelay = 1000;
        this.heartbeatInterval = null;
        this.listeners = {};
    }
    
    // 连接
    connect() {
        return new Promise((resolve, reject) => {
            this.ws = new WebSocket(this.url);
            
            this.ws.addEventListener('open', () => {
                console.log('连接成功');
                this.reconnectAttempts = 0;
                this.startHeartbeat();
                this.emit('open');
                resolve();
            });
            
            this.ws.addEventListener('message', (event) => {
                this.handleMessage(event.data);
            });
            
            this.ws.addEventListener('close', (event) => {
                console.log('连接关闭', event.code, event.reason);
                this.stopHeartbeat();
                this.emit('close', event);
                
                // 自动重连
                if (this.reconnectAttempts < this.maxReconnectAttempts) {
                    this.reconnect();
                }
            });
            
            this.ws.addEventListener('error', (error) => {
                console.error('连接错误', error);
                this.emit('error', error);
                reject(error);
            });
        });
    }
    
    // 重连
    reconnect() {
        this.reconnectAttempts++;
        console.log(`尝试重连 (${this.reconnectAttempts}/${this.maxReconnectAttempts})`);
        
        setTimeout(() => {
            this.connect().catch(err => {
                console.error('重连失败', err);
            });
        }, this.reconnectDelay * this.reconnectAttempts);
    }
    
    // 发送消息
    send(data) {
        if (this.ws && this.ws.readyState === WebSocket.OPEN) {
            const message = typeof data === 'string' ? data : JSON.stringify(data);
            this.ws.send(message);
            return true;
        }
        return false;
    }
    
    // 处理消息
    handleMessage(data) {
        try {
            const message = JSON.parse(data);
            this.emit('message', message);
        } catch (e) {
            this.emit('message', data);
        }
    }
    
    // 心跳机制
    startHeartbeat() {
        this.heartbeatInterval = setInterval(() => {
            if (this.ws && this.ws.readyState === WebSocket.OPEN) {
                this.ws.send(JSON.stringify({ type: 'ping' }));
            }
        }, 30000); // 每30秒发送一次心跳
    }
    
    stopHeartbeat() {
        if (this.heartbeatInterval) {
            clearInterval(this.heartbeatInterval);
            this.heartbeatInterval = null;
        }
    }
    
    // 关闭连接
    close() {
        this.stopHeartbeat();
        if (this.ws) {
            this.ws.close();
            this.ws = null;
        }
    }
    
    // 事件监听
    on(event, callback) {
        if (!this.listeners[event]) {
            this.listeners[event] = [];
        }
        this.listeners[event].push(callback);
    }
    
    // 触发事件
    emit(event, data) {
        if (this.listeners[event]) {
            this.listeners[event].forEach(callback => callback(data));
        }
    }
    
    // 获取状态
    getState() {
        if (!this.ws) return 'CLOSED';
        
        const states = ['CONNECTING', 'OPEN', 'CLOSING', 'CLOSED'];
        return states[this.ws.readyState];
    }
}

// 使用示例
const wsManager = new WebSocketManager('wss://echo.websocket.org');

wsManager.on('open', () => {
    console.log('WebSocket已连接');
});

wsManager.on('message', (data) => {
    console.log('收到消息:', data);
});

wsManager.on('close', () => {
    console.log('WebSocket已关闭');
});

wsManager.on('error', (error) => {
    console.error('WebSocket错误:', error);
});

wsManager.connect();

心跳机制

保持连接活跃

<!DOCTYPE html>
<html lang="zh-CN">
<head>
    <meta charset="UTF-8">
    <meta name="viewport" content="width=device-width, initial-scale=1.0">
    <title>WebSocket心跳机制</title>
    <style>
        body {
            font-family: Arial, sans-serif;
            max-width: 800px;
            margin: 50px auto;
            padding: 20px;
        }
        
        .status-panel {
            display: grid;
            grid-template-columns: repeat(3, 1fr);
            gap: 20px;
            margin: 20px 0;
        }
        
        .status-item {
            padding: 20px;
            background: #f9f9f9;
            border-radius: 8px;
            text-align: center;
        }
        
        .status-value {
            font-size: 32px;
            font-weight: bold;
            color: #007bff;
        }
        
        .status-label {
            color: #666;
            margin-top: 5px;
        }
        
        .log {
            background: #f9f9f9;
            padding: 15px;
            border-radius: 8px;
            max-height: 400px;
            overflow-y: auto;
            font-family: monospace;
        }
        
        .log-entry {
            padding: 5px 0;
            border-bottom: 1px solid #eee;
        }
        
        .ping {
            color: #ffc107;
        }
        
        .pong {
            color: #28a745;
        }
        
        .message {
            color: #007bff;
        }
        
        button {
            padding: 10px 20px;
            margin: 10px 5px;
            background: #007bff;
            color: white;
            border: none;
            border-radius: 5px;
            cursor: pointer;
        }
        
        button:hover {
            background: #0056b3;
        }
    </style>
</head>
<body>
    <h1>WebSocket心跳机制</h1>
    
    <div class="status-panel">
        <div class="status-item">
            <div class="status-value" id="connectionStatus">未连接</div>
            <div class="status-label">连接状态</div>
        </div>
        <div class="status-item">
            <div class="status-value" id="pingCount">0</div>
            <div class="status-label">发送心跳</div>
        </div>
        <div class="status-item">
            <div class="status-value" id="pongCount">0</div>
            <div class="status-label">收到响应</div>
        </div>
    </div>
    
    <div>
        <button onclick="connect()">连接</button>
        <button onclick="disconnect()">断开</button>
        <button onclick="sendMessage()">发送消息</button>
    </div>
    
    <h3>心跳日志</h3>
    <div class="log" id="log"></div>
    
    <script>
        let ws = null;
        let heartbeatTimer = null;
        let pingCount = 0;
        let pongCount = 0;
        
        // 连接
        function connect() {
            ws = new WebSocket('wss://echo.websocket.org');
            
            ws.addEventListener('open', function() {
                updateStatus('已连接');
                addLog('系统', '连接成功');
                startHeartbeat();
            });
            
            ws.addEventListener('message', function(event) {
                const data = event.data;
                
                // 检查是否是心跳响应
                if (data === 'pong' || data === 'ping') {
                    pongCount++;
                    document.getElementById('pongCount').textContent = pongCount;
                    addLog('PONG', '收到心跳响应', 'pong');
                } else {
                    addLog('消息', data, 'message');
                }
            });
            
            ws.addEventListener('close', function() {
                updateStatus('已断开');
                addLog('系统', '连接关闭');
                stopHeartbeat();
            });
            
            ws.addEventListener('error', function() {
                updateStatus('错误');
                addLog('错误', '连接错误');
            });
        }
        
        // 断开
        function disconnect() {
            stopHeartbeat();
            if (ws) {
                ws.close();
                ws = null;
            }
        }
        
        // 发送消息
        function sendMessage() {
            if (ws && ws.readyState === WebSocket.OPEN) {
                const message = 'Hello WebSocket!';
                ws.send(message);
                addLog('发送', message, 'message');
            }
        }
        
        // 启动心跳
        function startHeartbeat() {
            // 每30秒发送一次心跳
            heartbeatTimer = setInterval(() => {
                if (ws && ws.readyState === WebSocket.OPEN) {
                    ws.send('ping');
                    pingCount++;
                    document.getElementById('pingCount').textContent = pingCount;
                    addLog('PING', '发送心跳检测', 'ping');
                }
            }, 30000);
        }
        
        // 停止心跳
        function stopHeartbeat() {
            if (heartbeatTimer) {
                clearInterval(heartbeatTimer);
                heartbeatTimer = null;
            }
        }
        
        // 更新状态
        function updateStatus(status) {
            document.getElementById('connectionStatus').textContent = status;
        }
        
        // 添加日志
        function addLog(type, message, className = '') {
            const log = document.getElementById('log');
            const entry = document.createElement('div');
            entry.className = 'log-entry ' + className;
            entry.textContent = `[${new Date().toLocaleTimeString()}] ${type}: ${message}`;
            log.appendChild(entry);
            log.scrollTop = log.scrollHeight;
        }
    </script>
</body>
</html>

综合示例

<!DOCTYPE html>
<html lang="zh-CN">
<head>
    <meta charset="UTF-8">
    <meta name="viewport" content="width=device-width, initial-scale=1.0">
    <title>WebSocket聊天室 - 东巴文</title>
    <style>
        * {
            margin: 0;
            padding: 0;
            box-sizing: border-box;
        }
        
        body {
            font-family: 'Segoe UI', Tahoma, Geneva, Verdana, sans-serif;
            background: linear-gradient(135deg, #667eea 0%, #764ba2 100%);
            min-height: 100vh;
            display: flex;
            align-items: center;
            justify-content: center;
            padding: 20px;
        }
        
        .chat-container {
            width: 100%;
            max-width: 900px;
            background: white;
            border-radius: 15px;
            box-shadow: 0 10px 40px rgba(0, 0, 0, 0.2);
            overflow: hidden;
        }
        
        .chat-header {
            background: linear-gradient(135deg, #667eea 0%, #764ba2 100%);
            color: white;
            padding: 20px;
            display: flex;
            justify-content: space-between;
            align-items: center;
        }
        
        .chat-header h1 {
            font-size: 24px;
        }
        
        .connection-status {
            display: flex;
            align-items: center;
            gap: 10px;
        }
        
        .status-dot {
            width: 12px;
            height: 12px;
            border-radius: 50%;
            background: #dc3545;
            animation: pulse 2s infinite;
        }
        
        .status-dot.connected {
            background: #28a745;
        }
        
        @keyframes pulse {
            0%, 100% { opacity: 1; }
            50% { opacity: 0.5; }
        }
        
        .chat-body {
            display: flex;
            height: 600px;
        }
        
        .chat-sidebar {
            width: 250px;
            background: #f8f9fa;
            border-right: 1px solid #dee2e6;
            display: flex;
            flex-direction: column;
        }
        
        .sidebar-header {
            padding: 15px;
            background: #e9ecef;
            font-weight: bold;
            border-bottom: 1px solid #dee2e6;
        }
        
        .user-list {
            flex: 1;
            overflow-y: auto;
            padding: 10px;
        }
        
        .user-item {
            display: flex;
            align-items: center;
            padding: 10px;
            border-radius: 8px;
            margin-bottom: 5px;
            transition: background 0.3s;
        }
        
        .user-item:hover {
            background: #e9ecef;
        }
        
        .user-avatar {
            width: 40px;
            height: 40px;
            border-radius: 50%;
            background: linear-gradient(135deg, #667eea 0%, #764ba2 100%);
            color: white;
            display: flex;
            align-items: center;
            justify-content: center;
            font-weight: bold;
            margin-right: 10px;
        }
        
        .user-name {
            flex: 1;
        }
        
        .user-status {
            width: 8px;
            height: 8px;
            border-radius: 50%;
            background: #28a745;
        }
        
        .chat-main {
            flex: 1;
            display: flex;
            flex-direction: column;
        }
        
        .chat-messages {
            flex: 1;
            overflow-y: auto;
            padding: 20px;
        }
        
        .message {
            margin-bottom: 20px;
            display: flex;
            flex-direction: column;
        }
        
        .message.sent {
            align-items: flex-end;
        }
        
        .message.received {
            align-items: flex-start;
        }
        
        .message-sender {
            font-size: 12px;
            color: #666;
            margin-bottom: 5px;
        }
        
        .message-bubble {
            max-width: 70%;
            padding: 12px 16px;
            border-radius: 15px;
            word-wrap: break-word;
        }
        
        .message.sent .message-bubble {
            background: linear-gradient(135deg, #667eea 0%, #764ba2 100%);
            color: white;
        }
        
        .message.received .message-bubble {
            background: #e9ecef;
            color: #333;
        }
        
        .message-time {
            font-size: 11px;
            color: #999;
            margin-top: 5px;
        }
        
        .system-message {
            text-align: center;
            color: #666;
            font-size: 14px;
            margin: 10px 0;
        }
        
        .chat-input {
            padding: 15px;
            background: #f8f9fa;
            border-top: 1px solid #dee2e6;
            display: flex;
            gap: 10px;
        }
        
        .chat-input input {
            flex: 1;
            padding: 12px 15px;
            border: 1px solid #dee2e6;
            border-radius: 25px;
            outline: none;
            font-size: 14px;
        }
        
        .chat-input input:focus {
            border-color: #667eea;
        }
        
        .chat-input button {
            padding: 12px 25px;
            background: linear-gradient(135deg, #667eea 0%, #764ba2 100%);
            color: white;
            border: none;
            border-radius: 25px;
            cursor: pointer;
            font-weight: bold;
            transition: transform 0.3s;
        }
        
        .chat-input button:hover {
            transform: scale(1.05);
        }
        
        .chat-input button:disabled {
            opacity: 0.5;
            cursor: not-allowed;
            transform: none;
        }
        
        .typing-indicator {
            padding: 10px 20px;
            font-size: 14px;
            color: #666;
            font-style: italic;
        }
    </style>
</head>
<body>
    <div class="chat-container">
        <div class="chat-header">
            <h1>💬 WebSocket聊天室</h1>
            <div class="connection-status">
                <div class="status-dot" id="statusDot"></div>
                <span id="statusText">未连接</span>
            </div>
        </div>
        
        <div class="chat-body">
            <div class="chat-sidebar">
                <div class="sidebar-header">在线用户 (<span id="userCount">0</span>)</div>
                <div class="user-list" id="userList"></div>
            </div>
            
            <div class="chat-main">
                <div class="chat-messages" id="chatMessages"></div>
                <div class="typing-indicator" id="typingIndicator" style="display: none;"></div>
                <div class="chat-input">
                    <input type="text" id="messageInput" placeholder="输入消息..." disabled>
                    <button id="sendBtn" disabled>发送</button>
                    <button id="connectBtn">连接</button>
                </div>
            </div>
        </div>
    </div>
    
    <script>
        // 聊天室管理器
        class ChatRoom {
            constructor() {
                this.ws = null;
                this.username = '用户' + Math.floor(Math.random() * 10000);
                this.users = [];
                this.typingUsers = new Set();
                this.typingTimer = null;
                
                this.initElements();
                this.initEventListeners();
            }
            
            initElements() {
                this.statusDot = document.getElementById('statusDot');
                this.statusText = document.getElementById('statusText');
                this.userCount = document.getElementById('userCount');
                this.userList = document.getElementById('userList');
                this.chatMessages = document.getElementById('chatMessages');
                this.typingIndicator = document.getElementById('typingIndicator');
                this.messageInput = document.getElementById('messageInput');
                this.sendBtn = document.getElementById('sendBtn');
                this.connectBtn = document.getElementById('connectBtn');
            }
            
            initEventListeners() {
                this.connectBtn.addEventListener('click', () => this.connect());
                this.sendBtn.addEventListener('click', () => this.sendMessage());
                this.messageInput.addEventListener('keypress', (e) => {
                    if (e.key === 'Enter') {
                        this.sendMessage();
                    }
                });
                this.messageInput.addEventListener('input', () => this.handleTyping());
            }
            
            connect() {
                this.updateStatus('connecting');
                
                // 使用模拟的WebSocket服务器
                // 实际应用中替换为真实的WebSocket服务器地址
                this.ws = new WebSocket('wss://echo.websocket.org');
                
                this.ws.addEventListener('open', () => {
                    this.updateStatus('connected');
                    this.addSystemMessage('连接成功!欢迎来到聊天室');
                    
                    // 发送用户加入消息
                    this.send({
                        type: 'join',
                        username: this.username
                    });
                    
                    this.messageInput.disabled = false;
                    this.sendBtn.disabled = false;
                    this.connectBtn.textContent = '断开';
                    this.connectBtn.onclick = () => this.disconnect();
                });
                
                this.ws.addEventListener('message', (event) => {
                    this.handleMessage(event.data);
                });
                
                this.ws.addEventListener('close', () => {
                    this.updateStatus('disconnected');
                    this.addSystemMessage('已断开连接');
                    
                    this.messageInput.disabled = true;
                    this.sendBtn.disabled = true;
                    this.connectBtn.textContent = '连接';
                    this.connectBtn.onclick = () => this.connect();
                });
                
                this.ws.addEventListener('error', () => {
                    this.updateStatus('error');
                    this.addSystemMessage('连接错误');
                });
            }
            
            disconnect() {
                if (this.ws) {
                    this.ws.close();
                    this.ws = null;
                }
            }
            
            sendMessage() {
                const message = this.messageInput.value.trim();
                if (message && this.ws && this.ws.readyState === WebSocket.OPEN) {
                    this.send({
                        type: 'message',
                        username: this.username,
                        content: message,
                        timestamp: new Date().toISOString()
                    });
                    
                    this.addMessage(this.username, message, true);
                    this.messageInput.value = '';
                }
            }
            
            send(data) {
                if (this.ws && this.ws.readyState === WebSocket.OPEN) {
                    this.ws.send(JSON.stringify(data));
                }
            }
            
            handleMessage(data) {
                try {
                    const message = JSON.parse(data);
                    
                    switch (message.type) {
                        case 'join':
                            this.addSystemMessage(`${message.username} 加入了聊天室`);
                            this.addUser(message.username);
                            break;
                            
                        case 'leave':
                            this.addSystemMessage(`${message.username} 离开了聊天室`);
                            this.removeUser(message.username);
                            break;
                            
                        case 'message':
                            this.addMessage(message.username, message.content, false);
                            break;
                            
                        case 'typing':
                            this.handleTypingIndicator(message.username, message.isTyping);
                            break;
                            
                        case 'userList':
                            this.updateUserList(message.users);
                            break;
                    }
                } catch (e) {
                    // 如果不是JSON,直接显示文本
                    this.addMessage('服务器', data, false);
                }
            }
            
            addMessage(sender, content, isSent) {
                const messageDiv = document.createElement('div');
                messageDiv.className = `message ${isSent ? 'sent' : 'received'}`;
                
                const time = new Date().toLocaleTimeString();
                
                messageDiv.innerHTML = `
                    <div class="message-sender">${sender}</div>
                    <div class="message-bubble">${this.escapeHtml(content)}</div>
                    <div class="message-time">${time}</div>
                `;
                
                this.chatMessages.appendChild(messageDiv);
                this.chatMessages.scrollTop = this.chatMessages.scrollHeight;
            }
            
            addSystemMessage(content) {
                const messageDiv = document.createElement('div');
                messageDiv.className = 'system-message';
                messageDiv.textContent = content;
                this.chatMessages.appendChild(messageDiv);
                this.chatMessages.scrollTop = this.chatMessages.scrollHeight;
            }
            
            handleTyping() {
                // 发送正在输入状态
                this.send({
                    type: 'typing',
                    username: this.username,
                    isTyping: true
                });
                
                // 清除之前的定时器
                clearTimeout(this.typingTimer);
                
                // 3秒后发送停止输入状态
                this.typingTimer = setTimeout(() => {
                    this.send({
                        type: 'typing',
                        username: this.username,
                        isTyping: false
                    });
                }, 3000);
            }
            
            handleTypingIndicator(username, isTyping) {
                if (isTyping) {
                    this.typingUsers.add(username);
                } else {
                    this.typingUsers.delete(username);
                }
                
                if (this.typingUsers.size > 0) {
                    const users = Array.from(this.typingUsers);
                    const text = users.length === 1 
                        ? `${users[0]} 正在输入...`
                        : `${users.join(', ')} 正在输入...`;
                    
                    this.typingIndicator.textContent = text;
                    this.typingIndicator.style.display = 'block';
                } else {
                    this.typingIndicator.style.display = 'none';
                }
            }
            
            addUser(username) {
                if (!this.users.includes(username)) {
                    this.users.push(username);
                    this.updateUserList(this.users);
                }
            }
            
            removeUser(username) {
                this.users = this.users.filter(u => u !== username);
                this.updateUserList(this.users);
            }
            
            updateUserList(users) {
                this.users = users;
                this.userCount.textContent = users.length;
                
                this.userList.innerHTML = users.map(user => `
                    <div class="user-item">
                        <div class="user-avatar">${user.charAt(0).toUpperCase()}</div>
                        <div class="user-name">${user}</div>
                        <div class="user-status"></div>
                    </div>
                `).join('');
            }
            
            updateStatus(status) {
                const statusMap = {
                    connecting: { text: '连接中...', dot: '' },
                    connected: { text: '已连接', dot: 'connected' },
                    disconnected: { text: '未连接', dot: '' },
                    error: { text: '连接错误', dot: '' }
                };
                
                const { text, dot } = statusMap[status];
                this.statusText.textContent = text;
                this.statusDot.className = 'status-dot ' + dot;
            }
            
            escapeHtml(text) {
                const div = document.createElement('div');
                div.textContent = text;
                return div.innerHTML;
            }
        }
        
        // 初始化聊天室
        const chatRoom = new ChatRoom();
    </script>
</body>
</html>

最佳实践

1. 错误处理

// 推荐:完善的错误处理
ws.addEventListener('error', function(error) {
    console.error('WebSocket错误:', error);
    // 显示错误信息给用户
    showErrorMessage('连接发生错误,请稍后重试');
});

ws.addEventListener('close', function(event) {
    if (event.code !== 1000) {
        console.error('异常关闭:', event.code, event.reason);
        // 尝试重连
        reconnect();
    }
});

// 不推荐:没有错误处理
ws = new WebSocket(url);
// 没有任何错误处理

2. 连接管理

// 推荐:合理的连接管理
class WebSocketConnection {
    constructor(url) {
        this.url = url;
        this.ws = null;
        this.shouldReconnect = true;
    }
    
    connect() {
        this.ws = new WebSocket(this.url);
        
        this.ws.addEventListener('close', () => {
            if (this.shouldReconnect) {
                setTimeout(() => this.connect(), 3000);
            }
        });
    }
    
    disconnect() {
        this.shouldReconnect = false;
        if (this.ws) {
            this.ws.close();
        }
    }
}

// 不推荐:没有连接管理
ws = new WebSocket(url);
// 连接断开后不会重连

3. 消息队列

// 推荐:使用消息队列
class MessageQueue {
    constructor() {
        this.queue = [];
        this.ws = null;
    }
    
    send(message) {
        if (this.ws && this.ws.readyState === WebSocket.OPEN) {
            this.ws.send(message);
        } else {
            // 连接未打开,加入队列
            this.queue.push(message);
        }
    }
    
    onOpen() {
        // 发送队列中的消息
        while (this.queue.length > 0) {
            const message = this.queue.shift();
            this.ws.send(message);
        }
    }
}

东巴文点评:WebSocket开发中,错误处理、连接管理和消息队列是三个关键点,务必重视。

学习检验

知识点测试

问题1:WebSocket的readyState值为1表示什么状态?

A. 正在连接 B. 已连接 C. 正在关闭 D. 已关闭

<details> <summary>点击查看答案</summary>

答案:B

东巴文解释:WebSocket的readyState值:0-CONNECTING(正在连接)、1-OPEN(已连接)、2-CLOSING(正在关闭)、3-CLOSED(已关闭)。值为1表示连接已打开,可以发送和接收数据。

</details>

问题2:以下哪个方法用于关闭WebSocket连接?

A. ws.disconnect() B. ws.close() C. ws.end() D. ws.terminate()

<details> <summary>点击查看答案</summary>

答案:B

东巴文解释:WebSocket使用close()方法关闭连接。可以传入可选的code和reason参数:ws.close(1000, '正常关闭')

</details>

实践任务

任务:创建一个简单的实时股票价格监控应用,使用WebSocket接收股票价格更新并显示。

<details> <summary>点击查看参考答案</summary>
<!DOCTYPE html>
<html lang="zh-CN">
<head>
    <meta charset="UTF-8">
    <meta name="viewport" content="width=device-width, initial-scale=1.0">
    <title>实时股票价格监控</title>
    <style>
        body {
            font-family: Arial, sans-serif;
            max-width: 1000px;
            margin: 50px auto;
            padding: 20px;
            background: #f5f5f5;
        }
        
        h1 {
            text-align: center;
            color: #333;
        }
        
        .stock-grid {
            display: grid;
            grid-template-columns: repeat(auto-fit, minmax(250px, 1fr));
            gap: 20px;
            margin-top: 30px;
        }
        
        .stock-card {
            background: white;
            border-radius: 10px;
            padding: 20px;
            box-shadow: 0 2px 10px rgba(0, 0, 0, 0.1);
            transition: transform 0.3s;
        }
        
        .stock-card:hover {
            transform: translateY(-5px);
        }
        
        .stock-symbol {
            font-size: 24px;
            font-weight: bold;
            color: #333;
        }
        
        .stock-name {
            color: #666;
            margin-bottom: 15px;
        }
        
        .stock-price {
            font-size: 32px;
            font-weight: bold;
            margin: 10px 0;
        }
        
        .stock-change {
            font-size: 18px;
            font-weight: bold;
        }
        
        .stock-change.up {
            color: #28a745;
        }
        
        .stock-change.down {
            color: #dc3545;
        }
        
        .stock-chart {
            height: 80px;
            margin-top: 15px;
            background: #f9f9f9;
            border-radius: 5px;
            overflow: hidden;
        }
        
        .connection-status {
            text-align: center;
            padding: 10px;
            margin-bottom: 20px;
            border-radius: 5px;
        }
        
        .connection-status.connected {
            background: #d4edda;
            color: #155724;
        }
        
        .connection-status.disconnected {
            background: #f8d7da;
            color: #721c24;
        }
        
        .update-time {
            text-align: center;
            color: #666;
            margin-top: 20px;
        }
    </style>
</head>
<body>
    <h1>📈 实时股票价格监控</h1>
    
    <div class="connection-status disconnected" id="connectionStatus">
        未连接
    </div>
    
    <div class="stock-grid" id="stockGrid"></div>
    
    <div class="update-time" id="updateTime"></div>
    
    <script>
        // 模拟股票数据
        const stocks = [
            { symbol: 'AAPL', name: '苹果公司', price: 178.50 },
            { symbol: 'GOOGL', name: '谷歌', price: 141.80 },
            { symbol: 'MSFT', name: '微软', price: 378.90 },
            { symbol: 'AMZN', name: '亚马逊', price: 178.25 },
            { symbol: 'TSLA', name: '特斯拉', price: 248.50 },
            { symbol: 'META', name: 'Meta', price: 505.75 }
        ];
        
        // 股票监控器
        class StockMonitor {
            constructor() {
                this.ws = null;
                this.stockData = {};
                this.priceHistory = {};
                
                this.initStockData();
                this.renderStocks();
                this.connect();
            }
            
            initStockData() {
                stocks.forEach(stock => {
                    this.stockData[stock.symbol] = {
                        ...stock,
                        change: 0,
                        changePercent: 0
                    };
                    this.priceHistory[stock.symbol] = [stock.price];
                });
            }
            
            connect() {
                this.updateConnectionStatus('connecting');
                
                // 使用模拟数据(实际应用中连接真实WebSocket服务器)
                this.ws = new WebSocket('wss://echo.websocket.org');
                
                this.ws.addEventListener('open', () => {
                    this.updateConnectionStatus('connected');
                    this.startSimulation();
                });
                
                this.ws.addEventListener('message', (event) => {
                    try {
                        const data = JSON.parse(event.data);
                        if (data.type === 'stockUpdate') {
                            this.updateStock(data);
                        }
                    } catch (e) {
                        // 忽略非JSON消息
                    }
                });
                
                this.ws.addEventListener('close', () => {
                    this.updateConnectionStatus('disconnected');
                    // 重连
                    setTimeout(() => this.connect(), 3000);
                });
                
                this.ws.addEventListener('error', () => {
                    this.updateConnectionStatus('error');
                });
            }
            
            startSimulation() {
                // 模拟股票价格更新
                setInterval(() => {
                    const randomStock = stocks[Math.floor(Math.random() * stocks.length)];
                    const change = (Math.random() - 0.5) * 5; // -2.5 到 2.5
                    const newPrice = Math.max(0.01, this.stockData[randomStock.symbol].price + change);
                    
                    const update = {
                        type: 'stockUpdate',
                        symbol: randomStock.symbol,
                        price: newPrice,
                        timestamp: new Date().toISOString()
                    };
                    
                    // 模拟接收消息
                    this.updateStock(update);
                }, 1000);
            }
            
            updateStock(data) {
                const stock = this.stockData[data.symbol];
                if (stock) {
                    const oldPrice = stock.price;
                    stock.price = data.price;
                    stock.change = data.price - oldPrice;
                    stock.changePercent = (stock.change / oldPrice) * 100;
                    
                    // 更新价格历史
                    this.priceHistory[data.symbol].push(data.price);
                    if (this.priceHistory[data.symbol].length > 20) {
                        this.priceHistory[data.symbol].shift();
                    }
                    
                    this.updateStockCard(data.symbol);
                    this.updateUpdateTime();
                }
            }
            
            renderStocks() {
                const grid = document.getElementById('stockGrid');
                grid.innerHTML = Object.values(this.stockData).map(stock => `
                    <div class="stock-card" id="stock-${stock.symbol}">
                        <div class="stock-symbol">${stock.symbol}</div>
                        <div class="stock-name">${stock.name}</div>
                        <div class="stock-price">$${stock.price.toFixed(2)}</div>
                        <div class="stock-change ${stock.change >= 0 ? 'up' : 'down'}">
                            ${stock.change >= 0 ? '▲' : '▼'} 
                            ${Math.abs(stock.change).toFixed(2)} 
                            (${stock.changePercent >= 0 ? '+' : ''}${stock.changePercent.toFixed(2)}%)
                        </div>
                        <div class="stock-chart">
                            <canvas id="chart-${stock.symbol}" width="250" height="80"></canvas>
                        </div>
                    </div>
                `).join('');
            }
            
            updateStockCard(symbol) {
                const stock = this.stockData[symbol];
                const card = document.getElementById(`stock-${symbol}`);
                
                if (card) {
                    card.querySelector('.stock-price').textContent = `$${stock.price.toFixed(2)}`;
                    
                    const changeEl = card.querySelector('.stock-change');
                    changeEl.className = `stock-change ${stock.change >= 0 ? 'up' : 'down'}`;
                    changeEl.innerHTML = `
                        ${stock.change >= 0 ? '▲' : '▼'} 
                        ${Math.abs(stock.change).toFixed(2)} 
                        (${stock.changePercent >= 0 ? '+' : ''}${stock.changePercent.toFixed(2)}%)
                    `;
                    
                    // 更新图表
                    this.drawChart(symbol);
                }
            }
            
            drawChart(symbol) {
                const canvas = document.getElementById(`chart-${symbol}`);
                if (!canvas) return;
                
                const ctx = canvas.getContext('2d');
                const history = this.priceHistory[symbol];
                
                if (history.length < 2) return;
                
                // 清除画布
                ctx.clearRect(0, 0, canvas.width, canvas.height);
                
                // 计算范围
                const min = Math.min(...history);
                const max = Math.max(...history);
                const range = max - min || 1;
                
                // 绘制折线图
                ctx.beginPath();
                ctx.strokeStyle = history[history.length - 1] >= history[0] ? '#28a745' : '#dc3545';
                ctx.lineWidth = 2;
                
                history.forEach((price, i) => {
                    const x = (i / (history.length - 1)) * canvas.width;
                    const y = canvas.height - ((price - min) / range) * canvas.height * 0.8 - canvas.height * 0.1;
                    
                    if (i === 0) {
                        ctx.moveTo(x, y);
                    } else {
                        ctx.lineTo(x, y);
                    }
                });
                
                ctx.stroke();
            }
            
            updateConnectionStatus(status) {
                const statusEl = document.getElementById('connectionStatus');
                const statusMap = {
                    connecting: { text: '连接中...', className: 'disconnected' },
                    connected: { text: '已连接 - 实时更新中', className: 'connected' },
                    disconnected: { text: '未连接', className: 'disconnected' },
                    error: { text: '连接错误', className: 'disconnected' }
                };
                
                const { text, className } = statusMap[status];
                statusEl.textContent = text;
                statusEl.className = 'connection-status ' + className;
            }
            
            updateUpdateTime() {
                document.getElementById('updateTime').textContent = 
                    `最后更新: ${new Date().toLocaleTimeString()}`;
            }
        }
        
        // 初始化监控器
        const monitor = new StockMonitor();
    </script>
</body>
</html>
</details>

东巴文(db-w.cn) - 让编程学习更有趣、更高效!