本文目录导读:

我来详细讲解PHP长轮询的实现方法,包括基本实现、优化方案和注意事项。
基础长轮询实现
服务端代码(poll.php)
<?php
// 设置超时时间(秒)
set_time_limit(60);
// 配置
$timeout = 30; // 最长轮询时间
$startTime = time();
// 模拟数据检查(实际应用改为数据库查询)
function getNewData($lastId) {
// 这里用模拟数据代替实际数据库查询
$newData = [];
// 模拟有新数据(实际查询数据库)
$random = rand(1, 10);
if ($random > 7) { // 30%概率返回新数据
$newData = [
'id' => $lastId + 1,
'message' => '新消息 ' . date('H:i:s'),
'time' => time()
];
}
return $newData;
}
// 获取客户端传入的最后ID
$lastId = isset($_GET['lastId']) ? (int)$_GET['lastId'] : 0;
// 循环等待新数据
while (time() - $startTime < $timeout) {
// 检查是否有新数据
$newData = getNewData($lastId);
if (!empty($newData)) {
// 有数据,立即返回
echo json_encode([
'success' => true,
'data' => $newData
]);
exit;
}
// 休眠短暂时间,避免CPU占用过高
usleep(100000); // 休眠100ms
}
// 超时返回
echo json_encode([
'success' => false,
'message' => 'timeout',
'time' => date('H:i:s')
]);
客户端代码(JavaScript)
class LongPollingClient {
constructor(url, options = {}) {
this.url = url;
this.options = {
timeout: 30000,
onData: options.onData || function() {},
onError: options.onError || function() {},
onTimeout: options.onTimeout || function() {},
...options
};
this.lastId = 0;
this.isPolling = false;
}
// 开始轮询
start() {
this.isPolling = true;
this.poll();
}
// 停止轮询
stop() {
this.isPolling = false;
}
// 执行轮询
poll() {
if (!this.isPolling) return;
const controller = new AbortController();
const timeoutId = setTimeout(() => controller.abort(), this.options.timeout);
fetch(`${this.url}?lastId=${this.lastId}`, {
method: 'GET',
signal: controller.signal
})
.then(response => response.json())
.then(data => {
clearTimeout(timeoutId);
if (data.success && data.data) {
this.lastId = data.data.id;
this.options.onData(data.data);
} else if (!data.success && data.message === 'timeout') {
this.options.onTimeout(data);
}
// 继续下一轮轮询
this.poll();
})
.catch(error => {
clearTimeout(timeoutId);
this.options.onError(error);
// 错误后重试
setTimeout(() => this.poll(), 3000);
});
}
}
// 使用示例
const client = new LongPollingClient('/poll.php', {
onData: (data) => {
console.log('收到新数据:', data);
// 更新界面
updateUI(data);
},
onError: (error) => {
console.error('轮询出错:', error);
}
});
client.start();
数据库支持的长轮询
<?php
// 使用数据库检查新数据
function checkDatabase($db, $lastId, $userId) {
// 查询是否有新消息
$stmt = $db->prepare("
SELECT id, message, created_at
FROM messages
WHERE id > ? AND user_id = ?
ORDER BY id ASC
LIMIT 1
");
$stmt->execute([$lastId, $userId]);
$result = $stmt->fetch(PDO::FETCH_ASSOC);
return $result ?: null;
}
// 长轮询端点
$userId = $_GET['userId'] ?? 0;
$lastId = $_GET['lastId'] ?? 0;
$timeout = 30;
$startTime = time();
// 数据库连接
$pdo = new PDO('mysql:host=localhost;dbname=test', 'username', 'password');
while (time() - $startTime < $timeout) {
$newData = checkDatabase($pdo, $lastId, $userId);
if ($newData) {
echo json_encode([
'success' => true,
'data' => $newData
]);
exit;
}
// 释放数据库连接(防止连接过多)
$pdo = null;
// 等待1秒后重新连接
sleep(1);
// 重新连接数据库
$pdo = new PDO('mysql:host=localhost;dbname=test', 'username', 'password');
}
echo json_encode([
'success' => false,
'message' => 'timeout'
]);
Redis 实现长轮询
<?php
// 使用Redis做缓存和通知
$redis = new Redis();
$redis->connect('localhost', 6379);
$channel = 'user_' . $userId; // 用户专属频道
$timeout = 30;
// 订阅频道等待消息
$response = $redis->subscribe([$channel], function($redis, $channel, $message) {
// 收到消息时返回
echo $message;
return false; // 结束订阅
});
// 如果超时
if (!$response) {
echo json_encode([
'success' => false,
'message' => 'timeout'
]);
}
优化方案
1 使用MySQL UNLOCK
<?php
// 常规表UNLOCK检查
function checkWithUnlock($pdo, $lockName, $lastId) {
// 获取锁
$pdo->exec("SELECT GET_LOCK('$lockName', 1)");
// 查询数据
$stmt = $pdo->prepare("
SELECT id FROM some_table
WHERE id > ? ORDER BY id LIMIT 1
");
$stmt->execute([$lastId]);
$result = $stmt->fetch();
// 释放锁
$pdo->exec("SELECT RELEASE_LOCK('$lockName')");
return $result;
}
2 多线程处理(使用ReactPHP)
<?php
require 'vendor/autoload.php';
$loop = React\EventLoop\Factory::create();
$server = new React\Http\Server($loop, function ($request, $response) use (&$loop) {
$lastId = $request->getQueryParams()['lastId'] ?? 0;
// 等待新数据
$loop->addPeriodicTimer(1, function ($timer) use ($loop, $response, $lastId) {
$newData = checkForNewData($lastId);
if ($newData) {
$loop->cancelTimer($timer);
$response->writeHead(200);
$response->end(json_encode($newData));
}
});
// 超时控制
$loop->addTimer(30, function () use ($loop, $response) {
$response->writeHead(200);
$response->end(json_encode(['success' => false, 'message' => 'timeout']));
});
});
$server->listen(8080);
$loop->run();
完整示例:聊天室
服务端 chat_poll.php
<?php
session_start();
// 配置
$roomId = $_GET['room'] ?? 'default';
$lastMsgId = (int)($_GET['lastId'] ?? 0);
$username = $_SESSION['username'] ?? 'guest';
$timeout = 25;
$startTime = time();
// 数据库连接(演示用)
$pdo = new PDO('mysql:host=localhost;dbname=chat', 'root', '');
$pdo->setAttribute(PDO::ATTR_ERRMODE, PDO::ERRMODE_EXCEPTION);
// 检查新消息
while (time() - $startTime < $timeout) {
// 查询新消息
$stmt = $pdo->prepare("
SELECT m.*, u.username
FROM messages m
JOIN users u ON m.sender_id = u.id
WHERE m.id > ? AND m.room = ?
ORDER BY m.created_at ASC
LIMIT 10
");
$stmt->execute([$lastMsgId, $roomId]);
$messages = $stmt->fetchAll(PDO::FETCH_ASSOC);
if ($messages) {
// 返回新消息及最新ID
$maxId = array_reduce($messages, function($carry, $msg) {
return max($carry, $msg['id']);
}, $lastMsgId);
echo json_encode([
'success' => true,
'data' => [
'messages' => $messages,
'lastId' => $maxId
],
'timestamp' => time()
]);
exit;
}
// 等待1秒
sleep(1);
}
// 超时返回
echo json_encode([
'success' => false,
'message' => 'timeout',
'timestamp' => time()
]);
客户端聊天代码
class ChatClient extends LongPollingClient {
constructor(roomId, username) {
super('/chat_poll.php', {
onData: (data) => this.handleNewMessages(data),
onError: (error) => this.handleChatError(error)
});
this.roomId = roomId;
this.username = username;
}
// 发送消息
sendMessage(message) {
return fetch('/send_message.php', {
method: 'POST',
headers: {
'Content-Type': 'application/json',
},
body: JSON.stringify({
room: this.roomId,
message: message,
username: this.username
})
});
}
// 处理新消息
handleNewMessages(data) {
if (data.messages) {
data.messages.forEach(msg => {
this.displayMessage(msg);
});
// 更新lastId
if (data.lastId) {
this.lastId = data.lastId;
}
}
}
// 显示消息
displayMessage(message) {
const chatBox = document.getElementById('chat-messages');
const div = document.createElement('div');
div.className = 'message';
div.innerHTML = `
<strong>${message.username}:</strong> ${message.content}
<small>${new Date(message.created_at).toLocaleTimeString()}</small>
`;
chatBox.appendChild(div);
chatBox.scrollTop = chatBox.scrollHeight;
}
handleChatError(error) {
console.error('聊天错误:', error);
setTimeout(() => {
// 错误后自动重连
}, 5000);
}
}
// 初始化聊天
const chatClient = new ChatClient('general', 'Alice');
chatClient.start();
注意事项和优化建议
1 性能优化
// 1. 连接池:使用连接池减少数据库连接开销
$pool = new ConnectionPool();
// 2. 使用Redis做消息通知,减少数据库轮询
$redis->subscribe(['channel'], function($redis, $channel, $message) {
// 有消息立即返回
});
// 3. 异步处理(Swoole等)
$server = new Swoole\WebSocket\Server('0.0.0.0', 9501);
2 安全考虑
// 1. 验证用户身份
$token = $_GET['token'] ?? '';
if (!validateToken($token)) {
echo json_encode(['error' => 'unauthorized']);
exit;
}
// 2. 限制轮询频率
$redisKey = "poll_limit:{$userId}";
$requests = $redis->incr($redisKey);
if ($requests > 30) {
echo json_encode(['error' => 'too_many_requests']);
exit;
}
$redis->expire($redisKey, 60);
// 3. 防止SQL注入
// 使用PDO预处理语句
3 常见问题解决
// 1. 超时问题
ini_set('max_execution_time', 30);
ini_set('default_socket_timeout', 30);
// 2. 并发问题
// 确保唯一session锁不阻塞轮询
session_write_close(); // 关闭session写入
// 3. 资源释放
register_shutdown_function(function() {
// 清理资源
if (isset($pdo)) $pdo = null;
if (isset($redis)) $redis->close();
});
与其他技术比较
| 技术 | 优点 | 缺点 | 适用场景 |
|---|---|---|---|
| 轮询 | 简单、兼容性好 | 延迟高、资源浪费 | 低频数据更新 |
| 长轮询 | 低延迟、实现简单 | 连接占用 | 中等实时性需求 |
| WebSocket | 双向通信、低延迟 | 需专门服务器 | 高实时性应用 |
| SSE | 简单、自动重连 | 单向通信 | 服务器推送 |
长轮询是一个平衡方案,在不需要WebSocket复杂度的情况下提供较低延迟的实时通信,选择合适的实现方式需要考虑实际需求和服务器资源限制。