本文目录导读:

我来详细讲解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复杂度的情况下提供较低延迟的实时通信,选择合适的实现方式需要考虑实际需求和服务器资源限制。
版权声明:NBA直播
NBA直播
NBA直播
NBA直播
NBA直播
NBA直播
NBA直播
NBA直播
NBA直播
NBA直播
NBA直播
NBA直播
NBA直播
NBA直播
NBA直播
NBA直播
NBA直播
NBA直播
CBA直播
CBA直播
CBA直播
CBA直播
CBA直播
CBA直播
CBA直播
CBA直播
CBA直播
CBA直播
CBA直播
CBA直播
CBA直播
CBA直播
CBA直播
CBA直播
CBA直播
CBA直播
足球直播
足球直播
足球直播
足球直播
足球直播
足球直播
足球直播
足球直播
足球直播
足球直播
足球直播
足球直播
足球直播
足球直播
足球直播
足球直播
足球直播
足球直播
英超直播
英超直播
英超直播
英超直播
英超直播
英超直播
英超直播
英超直播
英超直播
英超直播
英超直播
英超直播
英超直播
英超直播
英超直播
英超直播
英超直播
英超直播