PHPSocket.IO 是一个 PHP 版本的 Socket.IO 服务端实现,由 Workerman 作者 walkor 开发,目的是让 PHP 开发者也能轻松构建实时应用,而无需切换到 Node.js。
它完全兼容 Socket.IO 客户端协议(支持 v1.3.0 ~ v2.x 的客户端),API 接口高度相似(on/emit/to/join 等),让前端 JS 代码几乎不用改,就能连接 PHP 后端。
composer require workerman/phpsocket.io
<?php
namespaceapp\http\controller;
usePHPSocketIO\SocketIO;
usethink\facade\Log;
useWorkerman\Worker;
class Server
{
/**
* PHPSocket.IO 服务启动入口
*
* @author: Tinywan(Shaobo Wan)
*/
publicfunction server()
{
// 用于记录每个 uid 的在线连接数(支持同一 uid 多端登录)
$uidConnectionMap = [];
// 记录上一次广播的在线用户数和页面数(当前代码中未使用,可用于后续扩展广播优化)
$last_online_count = 0;
$last_online_page_count = 0;
// 创建 PHPSocketIO 服务,监听 2120 端口(WebSocket 主端口)
$sender_io = new SocketIO(2120);
// ------------------- 客户端连接事件 -------------------
$sender_io->on('connection', function ($socket) {
Log::info('客户端发起连接');
// ------------------- 登录事件 -------------------
$socket->on('login', function ($uid) use ($socket) {
Log::info("客户端登录 uid: {$uid}");
global $uidConnectionMap;
// 防止重复登录
if (isset($socket->uid)) {
return;
}
$uid = (string) $uid;
// 初始化该 uid 的连接计数
if (!isset($uidConnectionMap[$uid])) {
$uidConnectionMap[$uid] = 0;
}
// 增加连接计数
++$uidConnectionMap[$uid];
// 将当前 socket 加入以 uid 为名的 room,便于定向推送
$socket->join($uid);
$socket->uid = $uid;
});
// ------------------- 断开连接事件 -------------------
$socket->on('disconnect', function () use ($socket) {
Log::info('客户端断开: ' . json_encode($socket));
if (!isset($socket->uid)) {
return;
}
global $uidConnectionMap;
// 减少连接计数,若 <=0 则移除该 uid 记录
if (--$uidConnectionMap[$socket->uid] <= 0) {
unset($uidConnectionMap[$socket->uid]);
}
});
});
// ------------------- Worker 启动后添加 HTTP 推送接口 -------------------
$sender_io->on('workerStart', function () use ($sender_io) {
// 创建一个独立的 HTTP Worker,用于接收其他 PHP 脚本的推送请求
// 端口 2121(内网通讯,不能与 2120 冲突)
$inner_http_worker = new Worker('http://0.0.0.0:2121');
$inner_http_worker->onMessage = function ($http_connection, $data) use ($sender_io) {
// 安全获取 POST 数据
$postData = $data['post'] ?? [];
$to = $postData['to'] ?? '';
$content = htmlspecialchars($postData['content'] ?? '', ENT_QUOTES, 'UTF-8');
Log::info("推送目标: " . json_encode($to));
if ($to) {
// 定向推送给指定 uid(通过 room)
$sender_io->to($to)->emit('new_msg', $content);
} else {
// 广播给所有连接
$sender_io->emit('new_msg', $content);
}
// 返回推送结果(离线用户返回 'offline')
if ($to && !isset($uidConnectionMap[$to])) {
$http_connection->send('offline');
} else {
$http_connection->send('ok');
}
};
// 启动 HTTP Worker 监听
$inner_http_worker->listen();
});
// 启动所有 Worker(包括 SocketIO 和 HTTP Worker)
Worker::runAll();
}
}
启动命令
php web_msg.php start-d
输出结果(守护进程模式)
Workerman[web_msg.php] start in DAEMON mode
──────────────────────────────────────────── WORKERMAN ────────────────────────────────────────────
Workerman version: 5.1.2 PHP version: 8.2.19
───────────────────────────────────────────── WORKERS ─────────────────────────────────────────────
proto user worker listen processes status
───── ──── ────────── ───────────────────────── ───────── ──────
tcp www PHPSocketIO socketIO://0.0.0.0:2120 1 [OK]
发送消息
/**
* 推送消息到 PHPSocket.IO(带错误日志)
*/
function send_web_msg($to_uid = 1, string $content = ''): string|array
{
if (empty($content)) {
return ['error_code' => 404, 'reason' => '缺少推送内容'];
}
$url = 'http://127.0.0.1:2121/';
$data = [
'type' => 'publish',
'content' => $content,
'to' => (string) $to_uid,
];
$ch = curl_init($url);
curl_setopt_array($ch, [
CURLOPT_POST => true,
CURLOPT_POSTFIELDS => http_build_query($data), // 更安全的 POST 方式
CURLOPT_RETURNTRANSFER => true,
CURLOPT_TIMEOUT => 5,
CURLOPT_CONNECTTIMEOUT => 2,
CURLOPT_HTTPHEADER => ['Expect:'],
]);
$response = curl_exec($ch);
$http_code = curl_getinfo($ch, CURLINFO_HTTP_CODE);
if (curl_errno($ch) || $http_code !== 200) {
$error = curl_error($ch) ?: "HTTP {$http_code}";
curl_close($ch);
// 可记录日志
// error_log("推送失败 to={$to_uid}: {$error}");
return ['error_code' => 500, 'reason' => "推送失败:{$error}"];
}
curl_close($ch);
return trim($response) ?: 'ok';
}
<!-- 推荐使用 jsDelivr 或 cdnjs 替代 bootcss(更稳定、全球加速) -->
<!-- Socket.IO 客户端:原版用 2.0.3,已较旧;建议升级到 4.x(兼容 PHPSocket.IO v2.x) -->
<script src="https://cdn.jsdelivr.net/npm/socket.io-client@4.8.1/dist/socket.io.min.js"></script>
<!-- notify.js 替代方案:Bootstrap 5 原生 Toasts 或其他现代库(如 Notyf、SweetAlert2) -->
<!-- 这里保留原版,但强烈建议替换为 Bootstrap Toasts(无需额外 JS) -->
<script src="https://cdn.jsdelivr.net/npm/notifyjs@3.0.0/dist/notify.min.js"></script>
<script>
/**
* Socket.IO 客户端初始化脚本
* - 连接到服务器
* - 以 uid 登录
* - 监听 new_msg 事件,更新通知区域并弹出提示
*
* @author Tinywan
* @version 2026 更新:现代 CDN + 更好实践
*/
$(document).ready(function () {
// 用户 ID(建议从后端动态注入或 localStorage 获取,避免硬编码)
const uid = "1"; // 或从 PHP echo 或 meta 标签获取:const uid = document.querySelector('meta[name="user-uid"]').content;
// Socket.IO 连接选项
const socket = io("https://www.tinywan.com", {
path: "/socket.io",
transports: ["websocket", "polling"], // 优先 WebSocket,fallback polling
reconnection: true, // 自动重连
reconnectionAttempts: 5,
timeout: 10000
});
// 连接成功事件
socket.on("connect", function () {
console.log("Socket.IO 连接成功");
socket.emit("login", uid); // 发送登录事件
});
// 连接错误处理(可选增强)
socket.on("connect_error", function (err) {
console.error("Socket.IO 连接失败:", err.message);
// 可显示 UI 提示:如 $("#notice-content").html("连接失败,请刷新页面");
});
// 接收新消息事件
socket.on("new_msg", function (msg) {
console.log("收到系统消息:", msg);
// 更新 DOM 显示
$("#notice-content").html(`系统提示:${msg}`);
// 弹出通知(使用 notify.js)
$(".notification.sticky").notify({
// 可自定义选项
className: "info", // 或 success / error / warning
autoHide: true,
clickToHide: true,
position: "top right",
hideDuration: 3000
});
});
});
</script>