
简介这是一份基于PHP的轻量级在线聊天室源码采用单文件、无数据库架构上传到支持PHP5或PHP7的服务器即可直接运行适合PHP初学者、Web实时通信学习者以及需要快速搭建临时聊天功能的开发者。压缩包体积仅8KB只包含1个php文件所有代码集中在一个文件中部署时无需配置数据库和额外扩展当前已有1869人学习/下载。源码完整给出了聊天室的核心逻辑包括无数据库方式暂存聊天记录、在线用户列表维护、基于轮询或长轮询的实时消息刷新、通过CSS自定义界面字体等同时兼顾PHP5与PHP7的语法兼容。通过运行这份代码可以直观理解一个轻量级Web应用从请求处理、数据暂存到前端动态更新的全链路实现思路也能为自行扩展为多用户群聊或接入存储层提供基础。 最近在建一个小型内部工具站需要给团队临时加一个网页聊天入口不想要数据库、不想要框架、更不想为了这个小功能去开启一堆常驻进程。翻来覆去把PHP的可选项都过了一遍最后干脆自己写了一个单文件无数据库的在线聊天室。这里我把它打包整理成了源码标题就叫“PHP 在线聊天室源码单文件无数据库版.zip”其实整个聊天室压缩后也就十几KB部署起来几乎零成本适合本地测试、内网小团队沟通也适合刚学PHP的人拿来做练手项目。这个版本最核心的思路就是一份PHP文件 一个存放消息的文本文件 浏览器端每隔两秒轮询一次就能跑出一个看起来像模像样的实时聊天室。省去了MySQL、Redis、WebSocket这些重型依赖只要服务器支持PHP 5.6以上版本就能直接跑。今天我把整个实现拆开讲清楚包括消息文件怎么设计、并发写入怎么不丢数据、为什么用轮询而不是长连接以及我踩过的那几个典型的坑都一并整理出来。1. 单文件无数据库方案的适用场景与选型逻辑很多朋友看到“单文件”三个字第一反应是这东西只能拿来玩不能上生产。我倒是觉得任何技术方案都要先看使用场景聊天室这类轻量工具恰恰有不少场景不需要数据库。1.1 为什么不建议硬套数据库传统聊天室教程都喜欢用MySQL存储消息再用PHP查询输出。但如果你仔细算一笔账一个小范围内的聊天室比如几十个人在工作群之外临时拉一个对话窗口一天消息量可能也就是几百条。为这几百条数据去维护一张数据表、配置数据库账号密码、处理连接池怎么看都是杀鸡用牛刀。再说了无数据库版本有一个天然优势备份和迁移都非常方便。整个聊天室就是一个文件夹拷走就能换服务器跑连配置都不用改。我在内网部署的时候直接扔到Apache的www目录下改一下文件权限就能用整个过程不超过一分钟。有朋友可能会问如果后续人多了、消息量大了怎么办我的答案是这个方案的定位就是“轻量”真到并发高、消息量大的阶段就该换WebSocket加消息队列那一套了。到时候单文件版本可以作为原型参考迁移成本也不算高。1.2 单文件架构是怎么做到的所谓单文件指的是核心功能全部打包进一个chat.php文件里包括登录昵称、显示历史消息、提交新消息这些逻辑。没有额外的css文件和js文件样式和脚本都以内联方式写在HTML标签里。消息存储则是用文件系统代替数据库。系统启动时会自动创建一个data.txt文件每一条消息以JSON格式追加到文件末尾。读取历史消息时直接读文件按行解析就行。用JSON而不是用自定义分隔符是为了避免用户消息里出现换行符、管道符等特殊字符导致解析错乱用标准的json_encode和json_decode最稳妥。需要特别提醒的是php默认的file_put_contents在追加模式下是“先打开、再写入、再关闭”三步多个用户同时发消息时确实有可能出现文件写入交错。这个问题我后面会详细讲怎么用flock文件锁来解决这里先埋个伏笔。2. 聊天室核心功能拆解既然要做一个能用的聊天室功能就不能只是“发句话、显示一下”这么简单。用户名、消息列表、自动刷新、输入频率控制这些细节都要处理好。下面我把几个核心模块逐个拆开讲一下各自的作用和实现思路。2.1 连接与身份识别不登录也能有名字这个版本没有做用户注册登录体系毕竟定位是轻量工具不需要密码复杂度校验和会话管理。进入聊天室时页面会弹出一个输入框让你填写昵称昵称会被存放在浏览器的localStorage里下次再进来就能自动识别。为什么用localStorage而不是PHP自带的session因为session依赖服务端存储会产生session文件在无状态API和纯静态页面频繁刷新时容易留下垃圾文件。localStorage存在浏览器端服务端无感知刷新页面后昵称还在但聊天室不需要知道“你是谁”只需要在发送消息时把昵称一起传过来就行这刚好契合了无状态设计。这里有一个细节昵称一定不能原样输出到页面上否则就埋了XSS漏洞。用户把昵称填成scriptalert(1)/script如果直接渲染每次加载页面都会弹出脚本这还不算严重的更狠的是可以借机读取其他用户的信息。我处理的方式很简单PHP端用htmlspecialchars过滤一遍前端数据插入DOM时也只用textContent而不是innerHTML双保险。2.2 消息读取与发送流程聊天的核心环节就两个读取消息、发送消息。读取消息走的是GET请求前端每两秒调用一次chat.php?actionhistory服务端读取data.txt里面的数据解析成JSON数组返回。前端拿到数组后和当前页面上已有的消息数量对比如果多了就追加新的如果少了说明消息被清理过那就全量刷新。这种“增量拉取兜底全量”的策略能减少不必要的DOM操作又不会因为消息文件被截断导致页面一直少内容。发送消息走的是POST请求前端把nickname和content两个字段提交上来。服务端先做合法性校验昵称不能超过20个字符消息不能为空也不能超过500个字符。校验通过后组装成一个标准的JSON对象再追加写入data.txt文件。文件里每一行都是一条完整的JSON消息互不干扰读取时按行解析就行天然规避了跨行污染问题。你可能注意到了我没有用CSRF Token。原因是在这种轻量内网工具里攻击者构造表单绕过你聊天室发消息的危害基本可以忽略不计。但如果你要放到公网建议至少加一个简单的Token校验别让外面的人随便往你的消息文件里灌垃圾数据。2.3 无刷新即时通信轮询与长连接的取舍标题里说是“在线聊天室”但没有用WebSocket选的是最朴素的轮询策略。每两秒一次定时器向服务器请求一次最新数据。这个方案的好处是简单可靠坏处是消息最多会有两秒延迟而且每个在线用户都会产生持续请求。为什么不直接上WebSocket一是单文件无扩展插件环境下php需要开启swoole扩展或用Workerman框架才能支撑WebSocket这对一个单文件小工具来说太重了。二是WebSocket需要处理断线重连、心跳保活、消息广播逻辑代码量直接翻倍维护成本上去了但在这个场景里并没能带来成倍的体验提升。如果是小团队内部用两秒延迟完全可以接受就像对讲机一样说完话停一下对方的声音就到了。轮询方案的另一个好处是页面上每个客户端看到的都是同一个数据源哪怕两个浏览器标签页同时开着也不会出现消息错乱。3. 完整代码实现与部署操作这一部分我就直接给出一个能跑的版本并对关键代码做逐段讲解。如果你手头有服务器或本地PHP环境照着敲一遍就能跑起来。3.1 代码结构和核心PHP函数整个源码压缩包解压后只有一个chat.php文件外加一个存放聊天数据用的data.txt如果不存在会自动创建。先用一个最简单的骨架把代码串起来。?php // 定义消息文件路径 define(MSG_FILE, __DIR__ . /data.txt); // 获取当前请求的动作 $action isset($_REQUEST[action]) ? $_REQUEST[action] : view; switch ($action) { case history: handleHistory(); break; case send: handleSend(); break; default: renderPage(); break; } function renderPage() { // 输出聊天室HTML页面 } function handleHistory() { header(Content-Type: application/json; charsetutf-8); echo json_encode(readMessages()); exit; } function handleSend() { $nickname trim($_POST[nickname] ?? ); $content trim($_POST[content] ?? ); // 校验和写入 }执行流程非常清楚每个请求进来先判断action参数为history就返回JSON消息列表为send就写入新消息默认直接渲染聊天室页面。文件开头的define把消息文件路径固定下来这样后面所有读写操作都用同一个常量避免路径写错。读取消息的函数是核心这里用了一个比较巧妙的方式直接按行读取文件每行json_decode一次然后放进数组。消息在文件里的顺序就是时间顺序所以数组下标越大的消息越新前端拿回去直接按顺序渲染就行。function readMessages() { if (!file_exists(MSG_FILE)) { return []; } $lines file(MSG_FILE, FILE_IGNORE_NEW_LINES | FILE_SKIP_EMPTY_LINES); $messages []; foreach ($lines as $line) { $msg json_decode($line, true); if (is_array($msg)) { $messages[] $msg; } } return $messages; }需要注意的是json_decode的第二个参数必须设为true否则返回的是对象而不是数组处理起来会多写不少箭头符号。还有一个容易踩的坑是如果某一行不是合法的JSON说明文件可能被外部截断或手工改坏了这时候直接跳过这一行比报错终止整个文件读取要友好得多。写入消息的函数就需要多一点防御性处理了。function writeMessage($nickname, $content) { $data [ nickname mb_substr($nickname, 0, 20, UTF-8), content mb_substr($content, 0, 500, UTF-8), time date(H:i:s), ]; $line json_encode($data, JSON_UNESCAPED_UNICODE) . \n; $fp fopen(MSG_FILE, a); if (flock($fp, LOCK_EX)) { fwrite($fp, $line); flock($fp, LOCK_UN); } fclose($fp); }这里我用的是fopen配合flock而不是直接file_put_contents就是为了处理并发写入。flock是PHP自带的一个轻量级文件锁机制LOCK_EX表示独占锁同一时间只允许一个进程写文件。如果没有这把锁两个用户几乎同时点发送键A的字符串和B的字符串就可能互相交错要么变成一行乱码要么互相覆盖。json_encode加上JSON_UNESCAPED_UNICODE参数很关键。如果不加中文会被转成\u5f20\u4e09这种一长串转义字符文件看起来费劲不说解析还多一步。加上这个参数data.txt直接就是可读的中文排查问题时可以直接打开文件看内容。3.2 HTML前端与JavaScript轮询PHP部分负责的是逻辑层前端HTML和JS要负责展示和交互。这个版本虽然没有单独的JS文件但内联脚本一样要写好不能因为是单文件就随便凑合。!DOCTYPE html html langzh-CN head meta charsetUTF-8 title轻量在线聊天室/title style body { font-family: Microsoft YaHei, sans-serif; max-width: 800px; margin: 20px auto; padding: 0 15px; } #msgBox { border: 1px solid #ccc; height: 450px; overflow-y: auto; padding: 10px; border-radius: 6px; background: #f9f9f9; } .msg { margin-bottom: 10px; } .nickname { font-weight: bold; color: #2c6cb0; margin-right: 8px; } .time { color: #999; font-size: 12px; } #formArea { margin-top: 12px; display: flex; gap: 8px; } #content { flex: 1; padding: 8px; border: 1px solid #ccc; border-radius: 4px; } button { padding: 8px 18px; border: none; background: #2c6cb0; color: #fff; border-radius: 4px; cursor: pointer; } /style /head body div idmsgBox/div div idformArea input typetext idnickname placeholder输入昵称 maxlength20 stylewidth: 120px; input typetext idcontent placeholder输入消息 maxlength500 button onclicksendMsg()发送/button /div script let lastCount 0; function loadMessages() { fetch(chat.php?actionhistory) .then(res res.json()) .then(data { if (!Array.isArray(data)) return; if (data.length lastCount) { for (let i lastCount; i data.length; i) { appendMessage(data[i]); } } else { // 服务端消息被清理过重新渲染 document.getElementById(msgBox).innerHTML ; data.forEach(appendMessage); } lastCount data.length; }) .catch(() {}); } function appendMessage(msg) { let box document.getElementById(msgBox); let div document.createElement(div); div.className msg; let nick document.createElement(span); nick.className nickname; nick.textContent (msg.nickname || 匿名) :; let content document.createElement(span); content.className content; content.textContent msg.content || ; div.appendChild(nick); div.appendChild(content); box.appendChild(div); box.scrollTop box.scrollHeight; } function sendMsg() { let nickname document.getElementById(nickname).value.trim(); let content document.getElementById(content).value.trim(); if (!content) return; if (!nickname) { nickname 游客 Math.floor(Math.random() * 1000); document.getElementById(nickname).value nickname; } let formData new FormData(); formData.append(nickname, nickname); formData.append(content, content); fetch(chat.php?actionsend, {method: POST, body: formData}) .then(res res.json()) .then(data { if (data.status ok) { document.getElementById(content).value ; loadMessages(); } else { alert(data.message || 发送失败); } }) .catch(() alert(网络异常发送失败)); } setInterval(loadMessages, 2000); loadMessages(); /script /body /html前端这段代码最重要的是appendMessage函数。我用createElement和textContent去渲染消息而不是用innerHTML拼接HTML字符串原因是innerHTML会解析消息里的HTML标签如果用户发了一段img srcx onerroralert(1)虽然字符串本身没危险但经过innerHTML注入后就会被浏览器当成真实标签执行。用textContent则永远是纯文本再危险的标签也只会乖乖显示成文字。轮询部分用的是fetch的Promise链写法比旧的XMLHttpRequest简洁不少。每两秒请求一次服务器返回的JSON数组里如果新增了消息就追加到页面底部同时自动滚动到最下方。这个自动滚动用了scrollTop scrollHeight注意要在消息插入DOM之后执行否则容器高度还没更新滚动位置就会差那么几像素。3.3 宝塔面板部署与环境配置绝大多数朋友的服务器都装了宝塔面板这里我就以宝塔环境为例讲一下部署步骤。其实过程异常简单新手上手也不会有压力。第一步在宝塔面板的网站菜单里添加一个站点PHP版本选择PHP 7.2以上都可以这个脚本兼容PHP 5.6到PHP 8.x建议直接用PHP 7.4或8.0性能和兼容性最平衡。第二步把chat.php上传到站点根目录或任意子目录比如/testsite/chat.php。第三步也是最容易忘的一步确保站点目录的写权限正常。Nginx或Apache运行用户对当前目录要有写权限否则data.txt创建不了聊天室就会报错。如果遇到页面能打开、消息却发不出去的情况十有八九是目录权限问题。宝塔面板里直接右键站点目录点权限把属主改为www权限设为755data.txt一旦生成就会自动归www所有后面就不会出幺蛾子了。还有一个小细节Nginx默认会对静态文件做缓存但你访问的是PHP动态脚本不受静态缓存影响。只是如果你把Nginx的sendfile开启了有时会碰到文件修改后前端拉取的数据还是旧的这时候清一下浏览器缓存或者直接CtrlF5强制刷新就能解决。4. 常见问题与排错实战每次分享代码评论区问得最多的就是“为什么我跑不起来”和“为什么会出现这个奇怪的问题”。下面这几个问题是我自己实测过程中真实遇到过的我直接按“问题、原因、解决方案”的方式整理出来方便你对照自查。4.1 data.txt创建失败或消息丢失第一次运行聊天室打开页面没问题但发送消息时提示系统错误或者刷新页面后发现刚才发的消息不见了。这类问题大概率是目录没有写权限导致的。检查方法用宝塔面板打开站点目录看目录权限是不是755属主不是www就改成www。改完之后手动在浏览器里访问一次chat.php?actionhistory如果能返回一个空数组[]说明PHP进程可以正常读取文件了。再发送一条测试消息然后查看同目录下是否新生成了data.txt同时确认文件大小不是0字节。还有一种消息丢失的情况data.txt存在大小正常但刷新后消息不显示。这是因为data.txt被改成了UTF-8带BOM格式第一行JSON最前面多了一个不可见字符\xEF\xBB\xBFjson_decode解析失败后整行被跳过。解决方法是把文件另存为UTF-8无BOM格式以后写文件时用file_put_contents的LOCK_EX参数PHP默认生成的就是无BOM文件问题自然消除。4.2 中文乱码聊天室页面能打开但页面上的中文字符全是乱码或者发送中文消息后前端显示的是\u5f20\u4e09这种ASCII转义序列。乱码问题集中在编码不一致上。HTML页面声明了UTF-8PHP默认的mb_internal_encoding却可能是ISO-8859-1导致PHP读取文件时把UTF-8的中文按单字节处理了。建议在脚本开头一行加上mb_internal_encoding(UTF-8)同时在HTML的head里确保有meta charsetUTF-8这样PHP和浏览器两边的编码就对齐了。至于\u5f20\u4e09这种转义显示就是我在写消息时忘了加JSON_UNESCAPED_UNICODE参数。检查一下writeMessage函数里的json_encode补上第二个参数即可。这里建议加一个兼容判断PHP 5.4以下版本的JSON_UNESCAPED_UNICODE常量不存在最低使用PHP 5.6就不会有问题了聊胜于无这个兼容性提醒还是写出来。4.3 消息实时性不够与轮询性能担忧用起来发现消息总是慢半拍别人发完消息自己这边要过一两秒才能看到。其实这个“慢半拍”在轮询方案里是正常现象两秒的间隔意味着延迟最多两秒。如果你觉得太慢可以把setInterval的间隔从2000毫秒调成1000毫秒。但我要警告一句如果聊天室在线人数多缩短轮询间隔会让服务器请求量翻倍反而可能拖慢整体响应。比较合理的方向是让轮询间隔自适应页面刚加载时用1秒轮询聊天气氛活跃时保持2秒一次如果检测到用户长时间没有说话可以拉长到5秒减少无效请求。或者加一个简单的随机偏移量让每个用户的轮询时间错开避免所有用户同时发起请求造成服务器瞬时压力。如果是真正追求实时性那就应该考虑WebSocket了。Workerman和Swoole都提供完整的PHP WebSocket方案但那就不是“单文件无数据库”这个范畴了。我觉得这里可以把边界讲清楚单文件方案解决的是“快速可用”不是“极致实时”。4.4 消息文件无限膨胀怎么办聊天室跑得久了data.txt会越来越大几万条消息占用的空间其实也没多大几万行JSON文本撑死也就几MB。但如果真有强迫症可以加一个清理机制把readMessages函数改成只读取最后500行或者写一个定时脚本每天清空一次data.txt。PHP本身没有“只读最后N行”的内置函数需要借助array_slice处理$lines file(MSG_FILE, FILE_IGNORE_NEW_LINES | FILE_SKIP_EMPTY_LINES); $lines array_slice($lines, -500);这样前端拿到的永远是最新500条消息文件就算有十万行实际展示的也只有几百条性能和体验都是一致的。如果你连文件膨胀都想避免可以在每天凌晨用crontab执行echo -n /site/data.txt直接清空文件。不过要注意清空操作执行前后最好用flock锁保护一下避免正好撞上有用户发消息导致的数据错乱。结语做这个小项目的时候我最深的感觉是很多场景下技术上最简单的方案反而最好用。单文件无数据库聊天室不需要安装扩展、不需要配置数据库、不需要常驻进程一行命令部署完毕拷走就能带走。它也许不适合百万级用户的公网聊天站但在内网工具、教学演示、临时讨论组这些场景里它廉价、可靠、好用这就够了。最后再分享一个小技巧如果是在本地测试环境跑这个聊天室想同时模拟多个用户可以复制浏览器地址打开两个隐身窗口一个窗口A昵称一个窗口B昵称两个窗口同时发消息就能直观看到消息同步效果。这种感觉比自己看着一个页面自说自话真实多了。本文还有配套的精品资源点击获取