刨根问底:`online_users.txt` 究竟从哪来?
刨根问底:online_users.txt 究竟从哪来?
你问到了整个后端最核心的问题:数据不是天上掉下来的,是程序在某个时刻写进去的。 下面从零开始,把这个文件的一生讲清楚。
一、先破除一个误解:服务器并不知道"谁在线"
你可能会以为,服务器像一个监控大屏,能看到"现在有 5 个人在用我的网站"。
不是的。
服务器本质上是一台被动的机器。它安静地待在那里,只有别人发请求给它,它才会动一下。没人请求,它什么都不知道。
所以,"在线人数"不是被"测量"出来的,而是被"推断"出来的。
二、那"在线"到底怎么定义?
既然服务器不知道谁在线,那它怎么算出"5"?
靠一个约定:"如果某个用户最近 5 分钟内有过请求,就算他'在线'。"
这就是绝大多数系统判定"在线"的方法。它不是"事实",是"推断"。
类比: 老师不知道学生是不是在教室里。但老师说:"如果一个人刚才 5 分钟内举过手,我就认为他在教室里。" 这个"举手"就是用户的 HTTP 请求。
三、文件的产生:三个时刻
现在看 online_users.txt 是怎么来的。它不是某个程序一次写好的,而是每次用户来访问时,被一点点改出来的。
时刻 1:用户第一次访问(登录)
谁触发: 用户打开浏览器,访问 http://mysite.com/login,输入账号密码,点"登录"。
服务器做什么:
<?php
// login.php 处理登录
$username = $_POST['username'];
// ...验证密码...
// 验证通过后:
// 1. 生成一个用户 ID(假设是 1001)
$userId = 1001;
// 2. 把这个用户写进 online_users.txt
file_put_contents(
'/var/data/online_users.txt',
$userId . "\n",
FILE_APPEND // 追加模式,不覆盖
);
// 3. 返回成功
echo "登录成功";
此刻文件内容:
1001
这是文件产生的第一刻。 是 login.php 主动写的。没有这个 file_put_contents,文件就不存在。
时刻 2:用户继续浏览页面
谁触发: 用户点了一个链接,访问 http://mysite.com/dashboard。
服务器做什么:
<?php
// dashboard.php 每次被访问时,都更新一下"最后活动时间"
$userId = 1001; // 从 session 或 token 里拿到
// 读当前文件内容
$lines = file('/var/data/online_users.txt', FILE_IGNORE_NEW_LINES);
// 如果用户已经在文件里,更新他的"时间戳"
// 如果不在,追加进去
$found = false;
foreach ($lines as $i => $line) {
if (str_starts_with($line, $userId . ':')) {
// 更新这条记录,加上新时间
$lines[$i] = $userId . ':' . time();
$found = true;
break;
}
}
if (!$found) {
$lines[] = $userId . ':' . time();
}
// 写回文件
file_put_contents('/var/data/online_users.txt', implode("\n", $lines) . "\n");
此刻文件内容:
1001:1735689600
关键变化: 文件里多了个时间戳。时间戳是"用户最后一次活动的时间"。
为什么需要时间戳?
- 因为服务器不能只在用户登录时记一次
- 用户登录后可能一直待着,也可能关了浏览器
- 所以每次访问都更新时间戳,让服务器知道他"还活着"
时刻 3:用户打开另一个页面,或每 30 秒 JS 自动发一次心跳
谁触发: 用户在页面上停留,前端 JS 每 30 秒发一个"心跳"请求:
// 前端 JS,每 30 秒执行一次
setInterval(() => {
fetch('/api/heartbeat', {
method: 'POST',
headers: { 'Authorization': 'Bearer ' + token }
});
}, 30000);
服务器做什么:
<?php
// heartbeat.php 只做一件事:更新当前用户的时间戳
$userId = 1001;
$lines = file('/var/data/online_users.txt', FILE_IGNORE_NEW_LINES);
foreach ($lines as $i => $line) {
if (str_starts_with($line, $userId . ':')) {
$lines[$i] = $userId . ':' . time();
break;
}
}
file_put_contents('/var/data/online_users.txt', implode("\n", $lines) . "\n");
此刻文件内容(假设现在有 5 个用户):
1001:1735689600
1002:1735689612
1003:1735689620
1004:1735689630
1005:1735689645
这就是那 5 个用户! 他们是通过每次请求时被写进去的。
四、$user_count 是怎么算出来的
现在再看"显示 5"那段代码,你就懂了:
open(my $fh, '<', '/var/data/online_users.txt');
my $count = 0;
while (my $line = <$fh>) {
chomp($line);
# 解析 "1001:1735689600" 这行
my ($uid, $last_active) = split(':', $line);
# 如果最后活动时间在 5 分钟以内,才算在线
if (time() - $last_active < 300) {
$count++;
}
}
close($fh);
这个程序做的:
- 打开文件(别人写好的)
- 逐行读
- 检查时间戳:如果时间太旧(超过 5 分钟),不算数
- 数有效行数 → 得到"在线人数"
所以"5"这个数字:
- 不是服务器"知道"的
- 是别人写进文件 + 这个程序数出来的
五、文件是怎么被"清理"的
用户关了浏览器,不再发心跳。他的时间戳会越来越旧。
服务器上通常有一个定时任务(cron):
# 每分钟执行一次 clean_online.php
* * * * * php /var/www/clean_online.php
<?php
// clean_online.php:删掉 5 分钟没活动的用户
$lines = file('/var/data/online_users.txt', FILE_IGNORE_NEW_LINES);
$newLines = [];
foreach ($lines as $line) {
[$uid, $last] = explode(':', $line);
if (time() - $last < 300) { // 5 分钟内还有活动
$newLines[] = $line; // 保留
}
// 否则丢弃
}
file_put_contents('/var/data/online_users.txt', implode("\n", $newLines) . "\n");
这个脚本定期运行,把"过期"的用户从文件里删掉。
六、文件的一生(时间线)
时刻 谁触发 文件内容
──────────────────────────────────────────────────────────
09:00 空文件 (不存在)
09:01 用户 1001 登录 "1001:1735689660"
09:03 用户 1002 登录 "1001:1735689660
1002:1735689780"
09:05 用户 1001 点击页面 "1001:1735689900"(时间戳更新)
1002:1735689780
09:07 用户 1003 登录 "1001:1735689900
1002:1735689780
1003:1735689960"
09:10 clean_online.php 跑 "1001:..." 保留(还在活动)
"1002:..." 5分钟没活动,删除
"1003:..." 保留
关键:
- 文件由每次用户请求一点点拼出来
- 文件由定时清理任务一点点修整
- 没有任何一个时刻,有一个"权威的数据库"告诉服务器"现在 5 个人在线"
"在线人数"完全是程序根据文件内容现算的。
七、更现代的做法:用 Redis 替代文件
用文件的问题: 多个用户同时写文件,会冲突;文件 I/O 慢;跨服务器不同步。
现代做法:用 Redis(内存数据库)
<?php
// 用户每次活动
$redis = new Redis();
$redis->connect('127.0.0.1', 6379);
// 把用户 ID 和时间戳存进 Redis 的 Sorted Set
// key = 'online_users'
// score = 当前时间戳
// member = 用户ID
$redis->zAdd('online_users', time(), '1001');
// 查询在线人数(5 分钟内有活动的)
$fiveMinAgo = time() - 300;
$count = $redis->zCount('online_users', $fiveMinAgo, '+inf');
echo "当前在线:$count";
Redis 内部做了什么?
- 每次
zAdd把用户和时间戳写进一个有序集合 zCount数出时间戳 > 5 分钟前的成员个数- 本质上和文件一样:先记录,再统计
区别: Redis 在内存里,快得多;支持并发;可以设自动过期(EXPIRE)。
八、一句话回答你的问题
"这个文件究竟是如何产生的?"
它不是某个程序一次性生成的,而是每一个用户的每一次请求,由后端代码(如
login.php、dashboard.php、heartbeat.php)调用了file_put_contents(..., FILE_APPEND),一次次追加进去的。没有用户的请求,文件永远是空的。
"服务器究竟如何知道多少人在线?"
它不知道。它每次需要显示"在线人数"时,会打开文件,逐行读,按时间戳过滤,数出有效行数。这个数字是现算的,不是存储的。
数据不是"事实",是"事件的累积"。
九、延伸理解:这跟"前后端分离"有什么关系?
现在你理解了"数据怎么来的",再回头看前后端分离,就有一个更深的视角:
| 层次 | 谁负责 | 例子 |
|---|---|---|
| 数据产生 | 用户行为 | 登录、点击、心跳 |
| 数据写入 | 后端代码 | zAdd、file_put_contents |
| 数据存储 | 数据库/文件 | Redis、MySQL、txt |
| 数据查询 | 后端代码 | zCount、SELECT COUNT(*) |
| 数据返回 | 后端 API | 返回 JSON {"online": 5} |
| 数据显示 | 前端 | Vue 渲染 {{ online }} |
"在线人数 5"这个数字,从"用户点击"到"页面显示",经过了 6 层。每一层都是某一方代码在干活。没有任何一层是"自动"的。
这也是前后端分离能成立的根本:
- 后端只管数据从哪来、怎么存、怎么查
- 前端只管数据怎么显示
- 两者通过 API 交接数据,各管一摊
你现在理解了整条链路。这就是从"一个数字"看到"整个系统"的思维方式。

浙公网安备 33010602011771号