PHP 实现心跳机制的几种方案
作者:快点好好学习吧
在PHP中实现心跳机制通常是为了保持一个长连接,比如在WebSocket或者某些长轮询场景中。但由于PHP本身是脚本语言,它并不像Java或C#那样直接支持多线程或长时间运行的服务,因此心跳机制在PHP中并不像在那些语言中那么常见。然而,我们可以模拟实现一种类似心跳的机制。
底层原理
心跳机制的底层原理主要是定期发送和接收消息来确认连接的状态。在客户端和服务器之间,每隔一段时间(比如每几秒),就会发送一个特殊的消息(心跳包),用于确认对方是否还在线。如果对方没有在规定时间内响应,我们就认为连接已经断开。
PHP实现
一、HTTP 轮询心跳(最常用,无需扩展)
适合普通 Web 站点统计在线人数、会话保活。
1. 后端接口
<?php
// heartbeat.php
$redis = new Redis();
$redis->connect('127.0.0.1', 6379);
$userId = $_POST['user_id'] ?? '';
if (empty($userId)) {
exit(json_encode(['code' => 1, 'msg' => 'user_id required']));
}
$now = time();
$ttl = 60; // 60s 无心跳视为离线
$key = "online:user:{$userId}";
// 单用户心跳时间(自动过期)
$redis->setex($key, $ttl, $now);
// 有序集合,便于批量统计和清理
$redis->zadd('online:users', $now, $userId);
exit(json_encode([
'code' => 0,
'server_time' => $now,
]));<?php
// offline.php —— 页面关闭时上报
$redis = new Redis();
$redis->connect('127.0.0.1', 6379);
$userId = $_POST['user_id'] ?? '';
if ($userId) {
$redis->del("online:user:{$userId}");
$redis->zrem('online:users', $userId);
}2. 在线判断与清理
class OnlineStatus
{
public function __construct(private Redis $redis, private int $threshold = 60) {}
public function isOnline(string $uid): bool
{
$last = $this->redis->get("online:user:{$uid}");
return $last && (time() - $last) < $this->threshold;
}
public function count(): int
{
$this->clean();
return $this->redis->zCard('online:users');
}
public function list(): array
{
$this->clean();
return $this->redis->zRange('online:users', 0, -1);
}
/** 清理超时用户(也可交给 crontab 每分钟跑一次) */
public function clean(): void
{
$this->redis->zRemRangeByScore('online:users', '-inf', time() - $this->threshold);
}
}⚠️ 如果用 Redis 键过期来自动判断离线,zrem 需要额外清理,可用 crontab 每分钟执行一次 clean()。
3. 前端
class Heartbeat {
constructor({ url, interval = 30000, maxRetry = 3, payload = {} }) {
this.url = url;
this.interval = interval; // 必须小于服务端 TTL(如 60s)
this.maxRetry = maxRetry;
this.payload = payload;
this.retry = 0;
this.timer = null;
}
beat() {
const body = new URLSearchParams(this.payload).toString();
fetch(this.url, {
method: 'POST',
headers: { 'Content-Type': 'application/x-www-form-urlencoded' },
body,
credentials: 'same-origin',
})
.then(r => r.json())
.then(res => {
if (res.code === 0) this.retry = 0;
else console.warn('心跳返回异常', res);
})
.catch(() => {
if (++this.retry >= this.maxRetry) {
this.stop();
this.onDead?.(); // 触发重连 / 提示
}
});
}
start() {
this.stop();
this.beat();
this.timer = setInterval(() => this.beat(), this.interval);
}
stop() {
clearInterval(this.timer);
this.timer = null;
}
}
// ---------- 使用 ----------
const hb = new Heartbeat({
url: '/heartbeat.php',
payload: { user_id: 123 },
interval: 30000,
});
hb.onDead = () => console.error('连接已断开');
hb.start();
// 页面切到后台时暂停,切回来立即补一次(浏览器会节流 setInterval)
document.addEventListener('visibilitychange', () => {
document.hidden ? hb.stop() : hb.start();
});
// 关闭页面前用 sendBeacon 上报离线(比同步 XHR 可靠)
window.addEventListener('beforeunload', () => {
navigator.sendBeacon('/offline.php', new URLSearchParams({ user_id: 123 }));
});要点:心跳间隔一般为服务端超时时间的 1/3 ~ 1/2,例如 TTL 60s → 心跳 20~30s。
二、WebSocket 心跳(Workerman)
长连接场景,推荐用协议层 ping/pong,Workerman 内置支持。
<?php
// server.php
use Workerman\Worker;
use Workerman\Timer;
use Workerman\Connection\TcpConnection;
require __DIR__ . '/vendor/autoload.php';
$ws = new Worker('websocket://0.0.0.0:8282');
$ws->count = 4;
// ===== 服务端主动心跳 =====
$ws->pingInterval = 25; // 每 25s 发一次 ping 帧
$ws->pingNotResponseLimit = 2; // 连续 2 次无响应则断开
$ws->pingData = ''; // 空 ping(浏览器会自动回 pong)
$ws->onConnect = function (TcpConnection $conn) {
$conn->lastActive = time();
echo "connect\n";
};
$ws->onMessage = function (TcpConnection $conn, $data) {
$msg = json_decode($data, true);
if (!is_array($msg)) return;
$conn->lastActive = time();
switch ($msg['type'] ?? '') {
case 'login':
$conn->uid = $msg['uid'] ?? null;
$conn->send(json_encode(['type' => 'login', 'code' => 0]));
break;
case 'ping': // 应用层心跳(可选,双保险)
$conn->send(json_encode(['type' => 'pong', 'time' => time()]));
break;
}
};
$ws->onClose = function (TcpConnection $conn) {
echo "close\n";
// 清理 Redis 在线状态等
};
// 兜底清理:超过 90s 无任何数据往来则断开
Timer::add(30, function () use ($ws) {
foreach ($ws->connections as $conn) {
if (time() - $conn->lastActive > 90) {
$conn->close();
}
}
});
Worker::runAll();启动:php server.php start -d
三、WebSocket 心跳(Swoole)
Swoole 提供开箱即用的 heartbeat_check_interval / heartbeat_idle_time:
<?php
use Swoole\WebSocket\Server;
use Swoole\WebSocket\Frame;
$server = new Server('0.0.0.0', 9502);
$server->set([
'heartbeat_check_interval' => 30, // 每 30s 扫描一次
'heartbeat_idle_time' => 90, // 90s 无数据则强制断开
]);
$server->on('Open', fn(Server $s, $req) => print("open {$req->fd}\n"));
$server->on('Message', function (Server $server, Frame $frame) {
$msg = json_decode($frame->data, true) ?: [];
if (($msg['type'] ?? '') === 'ping') {
$server->push($frame->fd, json_encode(['type' => 'pong', 'time' => time()]));
}
});
$server->on('Close', fn(Server $s, $fd) => print("close {$fd}\n"));
$server->start();四、前端 WebSocket 客户端(带指数退避重连)
class WSClient {
constructor(url, options = {}) {
this.url = url;
this.heartbeatInterval = options.heartbeatInterval ?? 25000;
this.pongTimeout = options.pongTimeout ?? 10000;
this.reconnectBase = options.reconnectBase ?? 1000;
this.maxDelay = options.maxDelay ?? 30000;
this.attempts = 0;
this.manualClose = false;
this.ws = null;
this.timer = null;
this.pongTimer = null;
}
connect() {
this.manualClose = false;
this.ws = new WebSocket(this.url);
this.ws.onopen = () => {
this.attempts = 0;
this.startHeartbeat();
this.onOpen?.();
};
this.ws.onmessage = (e) => {
let msg;
try { msg = JSON.parse(e.data); } catch { return; }
if (msg.type === 'pong') {
clearTimeout(this.pongTimer);
this.pongTimer = null;
return;
}
this.onMessage?.(msg);
};
this.ws.onclose = () => {
this.stopHeartbeat();
this.onClose?.();
if (!this.manualClose) this.reconnect();
};
}
startHeartbeat() {
this.stopHeartbeat();
this.timer = setInterval(() => {
if (this.ws?.readyState !== WebSocket.OPEN) return;
this.ws.send(JSON.stringify({ type: 'ping', t: Date.now() }));
// 超时未收到 pong → 主动断开触发重连
clearTimeout(this.pongTimer);
this.pongTimer = setTimeout(() => {
console.warn('pong 超时,判定断线');
this.ws.close();
}, this.pongTimeout);
}, this.heartbeatInterval);
}
stopHeartbeat() {
clearInterval(this.timer);
clearTimeout(this.pongTimer);
this.timer = this.pongTimer = null;
}
reconnect() {
// 指数退避 + 随机抖动,避免惊群
const delay = Math.min(
this.reconnectBase * 2 ** this.attempts++,
this.maxDelay
) + Math.random() * 1000;
console.log(`第 ${this.attempts} 次重连,${delay | 0}ms 后`);
setTimeout(() => this.connect(), delay);
}
close() {
this.manualClose = true;
this.stopHeartbeat();
this.ws?.close();
}
}
// ---------- 使用 ----------
const client = new WSClient('ws://your-host:8282');
client.onMessage = (msg) => console.log('业务消息', msg);
client.onOpen = () => console.log('已连接');
client.connect();注意事项
- Web服务器配置:长时间运行的PHP脚本可能需要调整Web服务器的配置,以防止超时断开连接。
- 资源消耗:长时间运行的脚本会占用服务器资源,需要确保服务器能够处理这种负载。
- 实际应用:在实际应用中,心跳机制的实现会更加复杂,可能涉及到WebSocket、长轮询、服务器推送等技术。
由于PHP主要用于Web开发,心跳机制的实现通常会与其他技术(如JavaScript、WebSocket等)结合使用。在真实的生产环境中,心跳机制的实现也会更加健壮和复杂,需要处理各种网络异常、重连逻辑等问题。
可以这样理解:心跳机制就像是你和好朋友之间每隔一段时间就互相发一个信号,确保对方还在线并且一切正常。如果你们很长时间没有互相发信号,可能就会担心对方是不是出了什么问题,这就是心跳机制的作用。
到此这篇关于PHP 实现心跳机制的几种方案的文章就介绍到这了,更多相关PHP 心跳机制内容请搜索脚本之家以前的文章或继续浏览下面的相关文章希望大家以后多多支持脚本之家!
