PHP长轮询怎么实现

wen PHP项目 2

本文目录导读:

PHP长轮询怎么实现

  1. 基础长轮询实现
  2. 数据库支持的长轮询
  3. Redis 实现长轮询
  4. 优化方案
  5. 完整示例:聊天室
  6. 注意事项和优化建议
  7. 与其他技术比较

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

抱歉,评论功能暂时关闭!