单文件PHP聊天室源码:无数据库实现与并发优化指南
简介这是一份面向PHP初学者与轻量级Web开发者的在线聊天室源码采用单文件、无数据库设计适合快速搭建临时或测试用途的聊天功能也可作为理解PHP请求处理与实时通信机制的学习素材。压缩包内仅1个php文件整体约8KB全部逻辑集中在一个脚本中部署时只需上传至支持PHP的服务器即可运行无需额外配置数据库环境。源码通过服务器端内存或文件暂存聊天记录并维护在线用户集合实现用户列表展示同时支持自定义字体样式且兼容PHP5与PHP7环境。消息传递可能借助轮询或长轮询完成让客户端与服务器保持近实时通信。目前已有1871人学习下载读者可从中掌握无数据库场景下的数据暂存思路、单文件结构组织方式以及跨版本兼容写法对理解Web实时通信与轻量应用开发有实际参考价值。1. 单文件 PHP 聊天室一个文件跑起来但别指望它扛住 50 人如果你手头只有一台最普通的虚拟主机没有 MySQL 权限甚至 phpMyAdmin 都进不去却想快速搭一个能用的在线聊天室那这份「PHP 在线聊天室源码单文件无数据库版」就是冲着你来的。它把后端逻辑、前端页面、消息存储全塞进一个.php文件里上传即用不需要建库、不需要改配置甚至不需要 Composer。听起来像玩具确实但它解决的是「从零到能聊」这一步——适合内部临时协作、活动弹幕墙、教学演示或者你只是想看看无数据库的聊天室到底怎么处理并发和消息持久化。我见过太多人一上来就上 Swoole 或 Node.js结果卡在环境配置上三天没跑通而这个单文件版本五分钟就能看到消息在浏览器里滚动。不过先泼盆冷水它用的是文件存储没有锁优化的话10 个人同时发言就可能丢消息。所以这篇笔记不吹它多强只讲清楚它怎么跑、参数怎么调、坑在哪以及什么时候你该果断换方案。2. 拆开这个单文件消息怎么存、轮询怎么发、并发怎么扛2.1 文件存储的读写模型为什么用 JSON 而不是 MySQL无数据库不等于没存储它只是把数据库换成了文件。常见做法是每次新消息到达时把消息追加到messages.json或chat.log里前端通过定时请求拉取最新内容。为什么选 JSON 而不是纯文本因为 JSON 能直接json_decode成数组前端拿到就能渲染省去解析分隔符的麻烦。但这里有个关键选择是每次读取整个文件还是只读最后 N 条单文件版通常用file_get_contents读全量再用array_slice取尾部。文件小的时候没问题一旦消息超过几千条每次轮询都读几百 KBCPU 和 I/O 都会上来。我一般会加一个MAX_LINES常量写入时用file按行读只保留最后 200 条再file_put_contents覆盖回去。这样文件不会无限膨胀代价是历史消息丢失——对临时聊天室来说这恰恰是优点。?php // 消息文件路径确保 PHP 有写权限 define(MSG_FILE, __DIR__ . /messages.json); // 最多保留的消息条数防止文件无限增长 define(MAX_MSGS, 200); function loadMessages() { if (!file_exists(MSG_FILE)) return []; $content file_get_contents(MSG_FILE); $msgs json_decode($content, true); return is_array($msgs) ? $msgs : []; } function saveMessage($user, $text) { $msgs loadMessages(); $msgs[] [ user htmlspecialchars($user, ENT_QUOTES, UTF-8), text htmlspecialchars($text, ENT_QUOTES, UTF-8), time date(H:i:s) ]; // 只保留最后 MAX_MSGS 条 if (count($msgs) MAX_MSGS) { $msgs array_slice($msgs, -MAX_MSGS); } file_put_contents(MSG_FILE, json_encode($msgs, JSON_UNESCAPED_UNICODE), LOCK_EX); }这段代码里LOCK_EX是必须的否则两个人同时写入会互相覆盖。htmlspecialchars防 XSS别省。JSON_UNESCAPED_UNICODE让中文不转成\uXXXX方便调试。参数MAX_MSGS根据你的内存和轮询频率调50 人以下用 200 足够人再多要降到 100 并加缓存。2.2 前端轮询与后端接口用 fetch 还是 setInterval单文件版通常把前端 HTML 和 PHP 处理逻辑写在一起用?actionsend和?actionget区分请求。前端用setInterval每 2 秒发一次 GET 拉取消息。这里有个细节轮询间隔太短服务器压力大太长消息延迟明显。我实测 2 秒是平衡点但如果你只是两个人对聊可以调到 1 秒。更稳的做法是用fetch加AbortController设超时避免某个请求卡死导致轮询堆积。// 每 2 秒拉取一次新消息 let lastCount 0; async function poll() { try { const res await fetch(chat.php?actionget, { signal: AbortSignal.timeout(3000) }); const data await res.json(); if (data.length lastCount) { renderMessages(data.slice(lastCount)); lastCount data.length; } } catch (e) { console.warn(轮询失败下次重试, e); } } setInterval(poll, 2000);AbortSignal.timeout(3000)是防止请求挂起3 秒没响应就取消下一轮继续。lastCount记录已渲染条数只追加新消息避免整个列表重绘。注意如果服务端做了MAX_MSGS截断lastCount会错位简单处理是每次全量渲染或者服务端返回一个递增的msgId。单文件版为了简洁通常全量渲染消息少时无感消息多了会闪屏。2.3 并发写入的锁与队列为什么你的消息会丢文件存储最大的坑是并发写。两个人同时提交A 读到文件B 也读到文件A 写入B 写入A 的消息就没了。LOCK_EX能解决一部分但 PHP 的file_put_contents加锁是阻塞的高并发下请求会排队响应变慢。更稳的方案是用flock显式加锁或者用fopen(a)追加模式——追加模式在大多数系统上是原子的但 JSON 格式要求整体覆盖追加会破坏结构。折中方案每条消息存一行 JSON用file_put_contents($file, $line, FILE_APPEND | LOCK_EX)读取时按行解析。这样写入是追加不会互相覆盖读取时file按行读再json_decode每行。代价是文件里会有无效行比如写入中断读取时要try/catch跳过。// 追加模式写入每行一条 JSON function appendMessage($user, $text) { $line json_encode([ user htmlspecialchars($user, ENT_QUOTES, UTF-8), text htmlspecialchars($text, ENT_QUOTES, UTF-8), time date(H:i:s) ], JSON_UNESCAPED_UNICODE) . \n; file_put_contents(MSG_FILE, $line, FILE_APPEND | LOCK_EX); } // 读取时逐行解析跳过坏行 function readMessages() { if (!file_exists(MSG_FILE)) return []; $lines file(MSG_FILE, FILE_IGNORE_NEW_LINES | FILE_SKIP_EMPTY_LINES); $msgs []; foreach ($lines as $line) { $item json_decode($line, true); if ($item) $msgs[] $item; } return array_slice($msgs, -MAX_MSGS); }FILE_APPEND保证追加LOCK_EX保证行级原子性。FILE_SKIP_EMPTY_LINES跳过空行。这种方案比整体 JSON 覆盖更抗并发但文件会越来越大需要定期清理。我一般加一个?actionclear接口手动清空或者用cron每天清一次。3. 从上传到跑通权限、编码、轮询间隔的实操配置3.1 上传后第一件事给目录写权限把chat.php传到虚拟主机后第一件事不是打开浏览器而是确认 PHP 有没有写权限。常见做法是chmod 755目录chmod 644文件但消息文件需要写所以要么让messages.json预先存在并chmod 666要么让 PHP 有创建文件的权限。如果用的是共享主机通常www-data或你的用户名就是运行用户直接chmod 777消息文件最省事但安全上不推荐。折中把消息文件放在sys_get_temp_dir()返回的临时目录或者用__DIR__ . /data/并确保data目录可写。# 创建数据目录并赋权Linux 虚拟主机 mkdir -p /path/to/chat/data chmod 755 /path/to/chat/data # 如果 PHP 运行用户不是你的用户需要 777 或改所有者 chown www-data:www-data /path/to/chat/data如果你没有 SSH只能用 FTP那就先在本地建一个空messages.json上传后通过主机面板的文件管理器把权限改成 666。注意有些主机禁止file_put_contents写非当前目录所以消息文件最好和chat.php同目录。3.2 编码与 XSS中文乱码和脚本注入的预防中文乱码通常出在两个地方PHP 文件本身不是 UTF-8 无 BOM或者json_encode没加JSON_UNESCAPED_UNICODE。前者用编辑器另存为 UTF-8 无 BOM 即可后者在代码里加常量。XSS 更危险用户输入scriptalert(1)/script直接存进 JSON前端innerHTML渲染就会执行。所以写入前必须htmlspecialchars输出时如果用的是textContent就安全用innerHTML则要再转义一次。我一般前后端都转双重保险。// 写入时转义 $text htmlspecialchars($_POST[text], ENT_QUOTES, UTF-8); // 输出时如果前端用 innerHTML再转一次 // 或者前端统一用 textContent前端渲染建议用document.createTextNode或textContent别用innerHTML拼字符串。如果一定要用模板用escapeHtml函数处理。3.3 轮询间隔与服务器负载2 秒还是 5 秒轮询间隔直接决定服务器压力。假设 20 人在线2 秒一次每分钟 600 次请求。虚拟主机通常限制并发连接数600 次/分钟可能触发 508 或 429。我实测 5 秒间隔在 20 人下比较稳但消息延迟明显。折中方案用setTimeout递归代替setInterval每次请求完成后再等 2 秒避免请求堆积。另外服务端可以加一个简单的缓存如果文件修改时间没变直接返回 304 或空数组减少 I/O。async function loop() { await poll(); setTimeout(loop, 2000); // 请求完成后再等 2 秒 } loop();服务端判断文件修改时间$mtime filemtime(MSG_FILE); if (isset($_GET[last]) $_GET[last] $mtime) { echo json_encode([]); exit; } // 返回消息时带上 mtime echo json_encode([mtime $mtime, msgs $msgs]);前端把mtime存下来下次请求带上没变就返回空省去读文件的开销。4. 避坑与排查消息丢失、乱码、权限拒绝的现场记录4.1 现象两个人同时发言只显示一条原因file_put_contents整体覆盖时没有加锁或者加了LOCK_EX但读取和写入之间有时间差后写入的覆盖了先写入的。解决改用追加模式每行一条 JSON读取时逐行解析。如果坚持整体 JSON必须在读取后、写入前加flock排他锁并且锁要覆盖整个读-改-写过程。4.2 现象中文显示为\u4f60\u597d原因json_encode默认转义 Unicode。解决加JSON_UNESCAPED_UNICODE参数。如果已经存了转义的数据读取时json_decode会自动还原但文件里看着难受。另外PHP 文件本身要保存为 UTF-8 无 BOM否则输出的 JSON 头会带 BOM前端解析失败。4.3 现象提示file_put_contents(): failed to open stream: Permission denied原因PHP 运行用户对消息文件或目录没有写权限。解决确认messages.json存在且权限为 666或目录权限为 777。如果主机禁用了file_put_contents改用fopenfwritefclose或者联系主机商开权限。共享主机常见限制是只能写tmp目录那就把消息文件路径改成sys_get_temp_dir() . /chat.json。4.4 现象轮询请求越来越多页面越来越卡原因setInterval在前一次请求未完成时又发起新请求导致请求堆积。解决改用setTimeout递归每次请求完成后再等固定间隔。同时加AbortController超时3 秒没响应就取消。另外检查服务端是否每次都在读整个大文件如果是加mtime缓存或限制MAX_MSGS。4.5 现象消息顺序错乱时间戳对不上原因多个人同时写入文件追加顺序和实际发言顺序不一致或者服务器时间不同步。解决单文件版很难做到严格顺序只能靠时间戳排序。写入时用microtime(true)存浮点时间读取时按时间排序。如果服务器时间不准前端渲染时用本地时间覆盖或者接受乱序——临时聊天室对顺序要求不高。5. 进阶用 SSE 替代轮询把延迟压到 1 秒内轮询的硬伤是延迟和无效请求。如果主机支持text/event-stream可以用 SSEServer-Sent Events让服务端主动推消息。PHP 实现 SSE 很简单设置Content-Type: text/event-stream然后循环读文件有新消息就echo data: ...\n\nflush()输出。前端用EventSource接收。这样延迟能压到 1 秒内而且没有无效请求。但坑也很明显PHP 的 SSE 会占用一个 PHP-FPM 进程10 个人在线就占 10 个进程虚拟主机通常限制 10-20 个进程人一多就 502。所以 SSE 只适合 5 人以下的小房间。如果非要上加一个sleep(1)控制循环频率并在客户端断开时break。// sse.php header(Content-Type: text/event-stream); header(Cache-Control: no-cache); $lastMtime 0; while (true) { if (connection_aborted()) break; clearstatcache(); $mtime filemtime(MSG_FILE); if ($mtime $lastMtime) { $lastMtime $mtime; $msgs readMessages(); echo data: . json_encode($msgs, JSON_UNESCAPED_UNICODE) . \n\n; flush(); } sleep(1); }前端const es new EventSource(sse.php); es.onmessage (e) { const msgs JSON.parse(e.data); renderMessages(msgs); };注意connection_aborted()检测客户端是否断开clearstatcache()清文件状态缓存否则filemtime一直返回旧值。sleep(1)降低 CPU 占用。如果主机不支持 SSE或者你发现进程数不够果断退回轮询。我自己的习惯是先跑轮询版确认功能正常再根据在线人数决定要不要换 SSE。换之前一定在本地用ab压测一下看进程数会不会爆。从那以后我每次部署这类单文件聊天室都会先改MAX_MSGS和轮询间隔再手动发 20 条消息测试并发写入最后才发给别人用。希望帮到你。本文还有配套的精品资源点击获取