刨根问底:`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);

这个程序做的:

  1. 打开文件(别人写好的)
  2. 逐行读
  3. 检查时间戳:如果时间太旧(超过 5 分钟),不算数
  4. 数有效行数 → 得到"在线人数"

所以"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 交接数据,各管一摊

你现在理解了整条链路。这就是从"一个数字"看到"整个系统"的思维方式。

posted @ 2026-10-13 14:44  华腾智算  阅读(5)  评论(0)    收藏  举报
https://damo.alibaba.com/ https://tianchi.aliyun.com/course?spm=5176.21206777.J_3941670930.5.87dc17c9BZNvLL