由于ThinkPHP是基于PHP的框架,而SSE(Server-Sent Events)是一种服务器向客户端推送事件的技术,我们可以通过创建一个控制器方法来输出SSE格式的响应。
在ThinkPHP中,我们可以通过设置响应头为`text/event-stream`,然后循环推送数据来实现SSE。
下面是一个简单的示例,演示如何在ThinkPHP中实现SSE:
1. 首先,创建一个控制器,比如`Sse.php`。
2. 在控制器中,创建一个方法,比如`index()`,用于处理SSE请求。
3. 在该方法中,设置响应头,然后循环发送数据。
注意:由于SSE是长连接,所以我们需要确保在推送数据时不会超时,并且需要手动刷新输出缓冲区。
效果:实现连接服务,断开服务,主动发送内容至大模型(模拟),前端动态追加显示内容
前端页面代码:
<!DOCTYPE html>
<html lang="zh-CN">
<head>
<meta charset="UTF-8">
<meta name="viewport" content="width=device-width, initial-scale=1.0">
<title>ThinkPHP SSE 示例</title>
<link href="https://cdn.jsdelivr.net/npm/bootstrap@5.3.0-alpha1/dist/css/bootstrap.min.css" rel="stylesheet">
<style>
body {
background-color: #f8f9fa;
font-family: 'Segoe UI', Tahoma, Geneva, Verdana, sans-serif;
}
.container {
max-width: 800px;
margin-top: 30px;
}
.card {
box-shadow: 0 4px 8px rgba(0,0,0,0.1);
margin-bottom: 20px;
border: none;
border-radius: 10px;
}
.card-header {
background-color: #4f68ff;
color: white;
border-radius: 10px 10px 0 0 !important;
}
.event-log {
height: 300px;
overflow-y: auto;
background-color: #2d3748;
color: #68d391;
font-family: 'Courier New', monospace;
padding: 15px;
border-radius: 5px;
}
.btn-action {
width: 120px;
}
.status-indicator {
display: inline-block;
width: 12px;
height: 12px;
border-radius: 50%;
margin-right: 8px;
}
.status-connected {
background-color: #28a745;
}
.status-disconnected {
background-color: #dc3545;
}
.event-item {
margin-bottom: 8px;
padding-bottom: 8px;
border-bottom: 1px solid #3c4858;
}
.timestamp {
color: #a0aec0;
font-size: 0.8em;
}
</style>
</head>
<body>
<div class="container">
<h1 class="text-center mb-4">ThinkPHP 服务器推送事件(SSE)示例</h1>
<div class="card">
<div class="card-header">
<h5 class="mb-0">连接控制</h5>
</div>
<div class="card-body">
<div class="d-flex justify-content-between align-items-center mb-3">
<div>
<span id="statusIndicator" class="status-indicator status-disconnected"></span>
<span id="statusText">未连接</span>
</div>
<div>
<button id="connectBtn" class="btn btn-success btn-action">连接</button>
<button id="disconnectBtn" class="btn btn-danger btn-action" disabled>断开</button>
</div>
</div>
<div class="mb-3">
<label for="eventType" class="form-label">事件类型</label>
<select class="form-select" id="eventType">
<option value="message">普通消息</option>
<option value="notification">通知</option>
<option value="alert">警报</option>
<option value="update">数据更新</option>
</select>
</div>
<div class="mb-3">
<label for="customMessage" class="form-label">自定义消息(可选)</label>
<input type="text" class="form-control" id="customMessage" placeholder="输入自定义消息内容">
</div>
<button id="sendEventBtn" class="btn btn-primary" disabled>发送事件</button>
</div>
</div>
<div class="card">
<div class="card-header">
<h5 class="mb-0">事件日志</h5>
</div>
<div class="card-body">
<div id="eventLog" class="event-log"></div>
</div>
</div>
<div class="card">
<div class="card-header">
<h5 class="mb-0">说明</h5>
</div>
<div class="card-body">
<p>这是一个使用ThinkPHP实现服务器推送事件(SSE)的示例。</p>
<p><strong>功能特点:</strong></p>
<ul>
<li>建立SSE连接并保持通信</li>
<li>发送不同类型的事件</li>
<li>自定义消息内容</li>
<li>实时显示服务器推送的事件</li>
</ul>
<p><strong>后端实现要点:</strong></p>
<ul>
<li>设置正确的HTTP头:Content-Type: text/event-stream</li>
<li>禁用缓存:Cache-Control: no-cache</li>
<li>保持连接:Connection: keep-alive</li>
<li>使用循环和ob_flush()实现实时输出</li>
</ul>
</div>
</div>
</div>
<script>
document.addEventListener('DOMContentLoaded', function() {
const eventLog = document.getElementById('eventLog');
const connectBtn = document.getElementById('connectBtn');
const disconnectBtn = document.getElementById('disconnectBtn');
const sendEventBtn = document.getElementById('sendEventBtn');
const statusIndicator = document.getElementById('statusIndicator');
const statusText = document.getElementById('statusText');
const eventTypeSelect = document.getElementById('eventType');
const customMessageInput = document.getElementById('customMessage');
let eventSource = null;
// 添加日志条目
function addLogEntry(message, type = 'info') {
const now = new Date();
const timestamp = now.toLocaleTimeString();
const logEntry = document.createElement('div');
logEntry.className = `event-item text-${type}`;
logEntry.innerHTML = `<span class="timestamp">[${timestamp}]</span> ${message}`;
eventLog.appendChild(logEntry);
eventLog.scrollTop = eventLog.scrollHeight;
}
// 建立SSE连接
function connectSSE() {
if (eventSource) {
return;
}
try {
// 这里使用相对路径,实际项目中应替换为你的ThinkPHP SSE端点
eventSource = new EventSource('/gbi/sse/search');
eventSource.onopen = function() {
statusIndicator.className = 'status-indicator status-connected';
statusText.textContent = '已连接';
connectBtn.disabled = true;
disconnectBtn.disabled = false;
sendEventBtn.disabled = false;
addLogEntry('SSE连接已建立', 'success');
};
eventSource.onmessage = function(event) {
const data = JSON.parse(event.data);
addLogEntry(`消息: ${data.message} (时间: ${data.time})`);
};
eventSource.addEventListener('notification', function(event) {
const data = JSON.parse(event.data);
addLogEntry(`通知: ${data.message} (优先级: ${data.priority})`, 'warning');
});
eventSource.addEventListener('alert', function(event) {
const data = JSON.parse(event.data);
addLogEntry(`警报: ${data.message} (级别: ${data.level})`, 'danger');
});
eventSource.addEventListener('update', function(event) {
const data = JSON.parse(event.data);
addLogEntry(`数据更新: ${data.message} (ID: ${data.id})`, 'info');
});
eventSource.onerror = function() {
addLogEntry('SSE连接错误', 'danger');
disconnectSSE();
};
} catch (error) {
addLogEntry(`连接失败: ${error.message}`, 'danger');
}
}
// 断开SSE连接
function disconnectSSE() {
if (eventSource) {
eventSource.close();
eventSource = null;
}
statusIndicator.className = 'status-indicator status-disconnected';
statusText.textContent = '未连接';
connectBtn.disabled = false;
disconnectBtn.disabled = true;
sendEventBtn.disabled = true;
addLogEntry('SSE连接已关闭', 'info');
}
// 发送自定义事件
function sendCustomEvent() {
if (!eventSource) {
addLogEntry('请先建立SSE连接', 'warning');
return;
}
const eventType = eventTypeSelect.value;
const customMessage = customMessageInput.value || `这是一条${eventType}类型的测试消息`;
// 在实际应用中,这里应该通过AJAX发送请求到服务器
// 服务器接收到请求后会通过SSE连接推送事件
addLogEntry(`已请求发送事件: ${eventType} - ${customMessage}`, 'info');
// 模拟服务器响应
setTimeout(() => {
const events = {
message: { message: customMessage, time: new Date().toLocaleTimeString() },
notification: { message: customMessage, priority: 'high' },
alert: { message: customMessage, level: 'warning' },
update: { message: customMessage, id: Math.floor(Math.random() * 1000) }
};
const event = new MessageEvent(eventType, { data: JSON.stringify(events[eventType]) });
if (eventType === 'message') {
eventSource.onmessage(event);
} else {
eventSource.dispatchEvent(event);
}
}, 500);
}
// 绑定按钮事件
connectBtn.addEventListener('click', connectSSE);
disconnectBtn.addEventListener('click', disconnectSSE);
sendEventBtn.addEventListener('click', sendCustomEvent);
// 初始化日志
addLogEntry('页面已加载,点击"连接"按钮开始SSE通信。');
});
</script>
</body>
</html>
ThinkPHP代码
<?php
declare (strict_types = 1);
namespace app\gbi\controller;
use app\BaseController;
use think\Request;
/**
* 测试sse
*/
class Sse extends BaseController
{
public function index()
{
return view();
}
public function search(Request $request)
{
// 设置SSE所需的HTTP头
header('Content-Type: text/event-stream');
header('Cache-Control: no-cache');
header('Connection: keep-alive');
header('Access-Control-Allow-Origin: *');
// 防止超时
set_time_limit(0);
// 获取客户端传递的lastEventId
$lastEventId = $request->header('Last-Event-ID');
// 发送初始注释消息(可选)
echo ": SSE连接已建立\n\n";
ob_flush();
flush();
$eventId = $lastEventId;
$counter = 0;
// 模拟持续发送事件
while (true) {
// 检查客户端是否仍然连接
if (connection_aborted()) {
break;
}
// 生成事件数据
$eventId++;
$data = [
'message' => '服务器时间: ' . date('Y-m-d H:i:s'),
'time' => date('H:i:s'),
'counter' => $counter
];
// 发送不同的事件类型
if ($counter % 10 == 0) {
// 每10秒发送一个通知事件
$this->sendEvent($eventId, 'notification', [
'message' => '系统通知 #' . $counter,
'priority' => 'medium',
'time' => date('H:i:s')
]);
} elseif ($counter % 7 == 0) {
// 每7秒发送一个警报事件
$this->sendEvent($eventId, 'alert', [
'message' => '系统警报 #' . $counter,
'level' => 'warning',
'time' => date('H:i:s')
]);
} elseif ($counter % 5 == 0) {
// 每5秒发送一个更新事件
$this->sendEvent($eventId, 'update', [
'message' => '数据已更新 #' . $counter,
'id' => rand(1000, 9999),
'time' => date('H:i:s')
]);
} else {
// 发送普通消息事件
$this->sendEvent($eventId, 'message', $data);
}
$counter++;
// 休眠1秒
sleep(1);
}
// 结束执行,避免ThinkPHP继续处理
exit;
}
/**
* 发送SSE事件
* @param int $id 事件ID
* @param string $event 事件类型
* @param array $data 事件数据
*/
private function sendEvent($id, $event, $data)
{
// 发送事件ID
echo "id: {$id}\n";
// 发送事件类型
if ($event && $event != 'message') {
echo "event: {$event}\n";
}
// 发送事件数据
echo "data: " . json_encode($data) . "\n\n";
// 刷新输出缓冲区
ob_flush();
flush();
}
/**
* 接收客户端发送的事件请求
*/
public function receive(Request $request)
{
// 在实际应用中,这里可以接收前端发送的事件请求
// 然后通过SSE连接向客户端推送事件
$eventType = $request->param('eventType', 'message');
$message = $request->param('message', '默认消息');
// 这里只是示例,实际应用中需要将事件推送给已连接的客户端
return json([
'code' => 200,
'message' => '事件已接收',
'eventType' => $eventType,
'data' => $message
]);
}
}