Skip to content

JavaScript 长连接

提示

在 JavaScript 中,"长连接"(Long Connection)通常指的是一种持久化的网络通信方式,与传统的短连接(每次请求 - 响应后断开连接)不同,

长连接会保持客户端与服务器之间的连接状态,允许双方在任意时间双向发送数据。常见方案:长轮询、短轮询、websocket

长连接的作用

长连接的主要优势是实时性高,避免了频繁建立连接的开销,适合需要实时数据交互的场景。WebSocket 是目前实现长连接的最优方案,得到了所有现代浏览器的支持

示例:WebSocket

HTML5 标准中定义的全双工通信协议

建立在 TCP 之上,通过一次握手后保持持久连接

允许服务器主动向客户端推送数据

典型用途:实时聊天、股票行情、在线游戏等

  1. 在客户端
html
<!DOCTYPE html>
<html>
<head>
    <title>WebSocket 客户端</title>
    <style>
        .container { max-width: 800px; margin: 20px auto; padding: 20px; }
        #messages { border: 1px solid #ccc; height: 300px; overflow-y: auto; padding: 10px; margin-bottom: 10px; }
        .message { margin: 5px 0; padding: 8px; border-radius: 4px; }
        .incoming { background-color: #e6f2ff; }
        .outgoing { background-color: #f0f0f0; text-align: right; }
        #inputForm { display: flex; gap: 10px; }
        #messageInput { flex: 1; padding: 8px; }
        button { padding: 8px 16px; background-color: #4CAF50; color: white; border: none; border-radius: 4px; cursor: pointer; }
        button:hover { background-color: #45a049; }
    </style>
</head>
<body>
    <div class="container">
        <h1>WebSocket 聊天</h1>
        <div id="messages"></div>
        <form id="inputForm">
            <input type="text" id="messageInput" placeholder="输入消息...">
            <button type="submit">发送</button>
        </form>
    </div>

    <script>
        // 连接到 WebSocket 服务器
        const ws = new WebSocket('ws://localhost:8080');
        
        // 获取 DOM 元素
        const messagesDiv = document.getElementById('messages');
        const inputForm = document.getElementById('inputForm');
        const messageInput = document.getElementById('messageInput');
        
        // 连接成功时触发
        ws.onopen = () => {
            console.log('WebSocket 连接已建立');
            addMessage('系统消息', '已连接到服务器', 'incoming');
        };
        
        // 接收到服务器消息时触发
        ws.onmessage = (event) => {
            console.log('收到消息:', event.data);
            addMessage('服务器', event.data, 'incoming');
        };
        
        // 连接关闭时触发
        ws.onclose = () => {
            console.log('WebSocket 连接已关闭');
            addMessage('系统消息', '与服务器的连接已关闭', 'incoming');
        };
        
        // 发生错误时触发
        ws.onerror = (error) => {
            console.error('WebSocket 错误:', error);
            addMessage('系统消息', `错误: ${error}`, 'incoming');
        };
        
        // 表单提交事件
        inputForm.addEventListener('submit', (e) => {
            e.preventDefault();
            const message = messageInput.value.trim();
            
            if (message && ws.readyState === WebSocket.OPEN) {
                // 发送消息到服务器
                ws.send(message);
                addMessage('', message, 'outgoing');
                messageInput.value = '';
            }
        });
        
        // 添加消息到界面
        function addMessage(sender, text, type) {
            const messageDiv = document.createElement('div');
            messageDiv.className = `message ${type}`;
            messageDiv.innerHTML = `<strong>${sender}:</strong> ${text}`;
            messagesDiv.appendChild(messageDiv);
            // 滚动到底部
            messagesDiv.scrollTop = messagesDiv.scrollHeight;
        }
    </script>
</body>
</html>
  1. 在服务端
js
// 安装依赖: npm install ws
const WebSocket = require('ws');

// 创建 WebSocket 服务器,监听 8080 端口
const wss = new WebSocket.Server({ port: 8080 });

console.log('WebSocket 服务器运行在 ws://localhost:8080');

// 当有客户端连接时
wss.on('connection', (ws) => {
    console.log('新客户端已连接');
    
    // 向客户端发送欢迎消息
    ws.send('欢迎连接到 WebSocket 服务器!');
    
    // 当收到客户端消息时
    ws.on('message', (message) => {
        console.log(`收到消息: ${message}`);
        
        // 将消息广播给所有连接的客户端
        wss.clients.forEach((client) => {
            if (client.readyState === WebSocket.OPEN) {
                client.send(message.toString());
            }
        });
    });
    
    // 当连接关闭时
    ws.on('close', () => {
        console.log('客户端已断开连接');
    });
    
    // 发生错误时
    ws.on('error', (error) => {
        console.error('WebSocket 错误:', error);
    });
});

示例:长轮询

工作原理:客户端发送请求后,服务器不会立即响应,而是等待有数据或超时才返回。客户端收到响应后立即再次发起请求,保持连接 "长开"

客户端:每次请求成功后或者失败后再次发送请求

服务端:服务端使用 setInterval 定时查库,如果有新数据则直接返回,如果没有则等待超时

  1. 在客户端
js
// 客户端长轮询实现
function longPoll() {
  // 发起请求
  fetch('/long-poll')
    .then(response => {
      if (!response.ok) {
        throw new Error('网络响应不正常');
      }
      return response.json();
    })
    .then(data => {
      // 处理服务器返回的数据
      console.log('收到数据:', data);
      document.getElementById('result').textContent = JSON.stringify(data);
      
      // 数据处理完成后,立即发起下一次轮询
      longPoll();
    })
    .catch(error => {
      console.error('轮询出错:', error);
      // 出错后延迟一段时间再重试,避免频繁请求
      setTimeout(longPoll, 3000);
    });
}

// 页面加载完成后开始第一次轮询
window.onload = longPoll;
  1. 在服务端
js
// 服务器端实现(使用Express框架)
const express = require('express');
const app = express();
const port = 3000;

// 存储等待中的请求
let pendingRequests = [];

// 模拟数据更新
function simulateDataUpdate() {
  setInterval(() => {
    const newData = {
      timestamp: new Date().toISOString(),
      message: '这是一条新消息',
      random: Math.random()
    };
    
    // 如果有等待中的请求,立即响应
    if (pendingRequests.length > 0) {
      const resolve = pendingRequests.shift();
      resolve(newData);
    }
  }, 5000); // 每5秒产生一次数据更新
}

// 启动模拟数据更新
simulateDataUpdate();

// 长轮询接口
app.get('/long-poll', (req, res) => {
  // 设置超时时间(略小于客户端超时)
  const timeout = 30000; // 30秒
  
  // 存储当前请求的resolve函数,等待数据更新
  const promise = new Promise((resolve) => {
    pendingRequests.push(resolve);
    
    // 超时处理
    setTimeout(() => {
      // 从等待队列中移除
      const index = pendingRequests.indexOf(resolve);
      if (index !== -1) {
        pendingRequests.splice(index, 1);
      }
      // 返回空数据,表示超时
      resolve(null);
    }, timeout);
  });
  
  // 当有数据或超时时响应客户端
  promise.then(data => {
    res.json(data || { timeout: true });
  });
});

// 提供客户端页面
app.get('/', (req, res) => {
  res.send(`
    <!DOCTYPE html>
    <html>
    <head>
      <title>长轮询示例</title>
    </head>
    <body>
      <h1>长轮询数据接收</h1>
      <div id="result"></div>
      <script src="/client.js"></script>
    </body>
    </html>
  `);
});

// 静态文件服务
app.use(express.static('public'));

app.listen(port, () => {
  console.log(`长轮询服务器运行在 http://localhost:${port}`);
});

示例:短轮询

工作原理:客户端设置一个定时器,按照固定间隔(如 2 秒)向服务器发送请求

在客户端:在ajax 的请求的 finally 回调中使用 setTimeout 定时再次发送 ajax 请求

在服务端:无需额外设置

js
// 短轮询客户端实现
class ShortPollingClient {
   constructor(url, interval = 3000) {
      this.url = url;
      this.interval = interval; // 轮询间隔,默认3秒
      this.polling = false;
      this.timer = null;
   }

   // 开始轮询
   start() {
      if (this.polling) return;

      this.polling = true;
      console.log('开始短轮询...');
      this.fetchData(); // 立即执行一次
   }

   // 停止轮询
   stop() {
      this.polling = false;
      if (this.timer) {
         clearTimeout(this.timer);
         this.timer = null;
      }
      console.log('已停止短轮询');
   }

   // 发送请求获取数据
   async fetchData() {
      if (!this.polling) return;

      try {
         const response = await fetch(this.url);
         if (!response.ok) {
            throw new Error(`HTTP error! status: ${response.status}`);
         }

         const data = await response.json();
         this.handleNewData(data);
      } catch (error) {
         console.error('轮询出错:', error);
      } finally {
         // 无论成功失败,都设置下一次轮询
         if (this.polling) {
            this.timer = setTimeout(() => this.fetchData(), this.interval);
         }
      }
   }

   // 处理新数据的回调函数
   handleNewData(data) {
      console.log('收到新数据:', data);
      // 这里可以添加更新UI等逻辑
   }
}

// 使用示例
// 启动轮询
const client = new ShortPollingClient('/api/data', 2000); // 每2秒轮询一次
client.start();

// 5秒后停止轮询(仅作示例)
setTimeout(() => {
   client.stop();

   // 10秒后再次启动轮询(仅作示例)
   setTimeout(() => {
      client.start();
   }, 10000);
}, 5000);