You need to enable JavaScript to run this app.
最新活动
大模型
产品
解决方案
定价
生态与合作
支持与服务
开发者
了解我们

咨询解决JSON格式访客数据文件(guest.txt)意外损坏问题的具体方案

咨询解决JSON格式访客数据文件(guest.txt)意外损坏问题的具体方案

问题背景还原

先整理下你遇到的场景和提供的信息:

我的PHP处理代码

$serverGuestFile = fopen($_SERVER['DOCUMENT_ROOT'] . '/server/guest.txt', "r");
$serverGuestFileData = fread($serverGuestFile, filesize($_SERVER['DOCUMENT_ROOT'] . '/server/guest.txt'));
fclose($serverGuestFile);
$serverGuestFileData = json_decode($serverGuestFileData, true);

$_SESSION['guestID'] = array_search($ipAddress, array_column($serverGuestFileData, 0));

if ($_SESSION['guestID'] === false) {
    array_push($serverGuestFileData, array($ipAddress, $time));
    $_SESSION['guestID'] = sizeof($serverGuestFileData) - 1;
    $serverGuestFileData = json_encode($serverGuestFileData);
    $serverGuestFile = fopen($_SERVER['DOCUMENT_ROOT'] . '/server/guest.txt', "w");
    fwrite($serverGuestFile, $serverGuestFileData);
    fclose($serverGuestFile);
} else {
    $serverGuestFileData[$_SESSION['guestID']][1] = $time;
    $serverGuestFileData = json_encode($serverGuestFileData);
    $serverGuestFile = fopen($_SERVER['DOCUMENT_ROOT'] . '/server/guest.txt', "w");
    fwrite($serverGuestFile, $serverGuestFileData);
    fclose($serverGuestFile);
}

正常的guest.txt内容

[["::1",1755951136],["158.62.69.152",1752225691],["54.36.148.143",1756166177],["124.222.209.139",1753035977]]

损坏的guest.txt内容

[["::1",1755951136],["158.62.69.152",1752225691],["54.36.148.143",1756166177]][["124.222.209.139",1753035977]]

错误点:本该以单个]结尾,结果出现了多余的方括号拼接,导致JSON格式完全失效。

我一直确信自己的代码逻辑没问题,距离上次文件损坏已经过去好几个月了,这次怀疑是有人攻击网站导致的。目前手动修正多余的]后网站已恢复,想知道未来怎么从根源解决这个问题?


问题根源分析

先给你吃个定心丸:这大概率不是攻击导致的,而是并发写入冲突的经典问题

当有多个访客同时访问网站时,会出现以下致命场景:

  1. 访客A的请求先打开guest.txt读取数据,还没来得及写入;
  2. 访客B的请求也同时打开了同一个文件,读取到了和A一样的旧数据;
  3. 两个请求的写入操作“撞车”,要么后写入的覆盖先写入的内容,要么更糟——两个请求的写入内容被强制拼接,就像你看到的两个JSON数组连在一起、多了方括号的情况。

你的代码里没有任何防止并发读写冲突的机制,所以在访问量突然上升时,就会触发文件损坏。


具体解决方案

下面给你几个从易到难的方案,你可以根据自己的需求选择:

方案1:给文件加排他锁(最快实现)

在打开文件时使用排他锁,确保同一时间只有一个请求能读写这个文件。修改后的代码如下:

$filePath = $_SERVER['DOCUMENT_ROOT'] . '/server/guest.txt';

// 打开文件并加排他锁,直到操作完成才释放
$serverGuestFile = fopen($filePath, "r+");
flock($serverGuestFile, LOCK_EX);

// 读取文件内容
rewind($serverGuestFile);
$serverGuestFileData = fread($serverGuestFile, filesize($filePath));
$serverGuestFileData = json_decode($serverGuestFileData, true);

// 处理访客数据逻辑
$_SESSION['guestID'] = array_search($ipAddress, array_column($serverGuestFileData, 0));
if ($_SESSION['guestID'] === false) {
    array_push($serverGuestFileData, array($ipAddress, $time));
    $_SESSION['guestID'] = sizeof($serverGuestFileData) - 1;
} else {
    $serverGuestFileData[$_SESSION['guestID']][1] = $time;
}

// 写入更新后的数据
$serverGuestFileData = json_encode($serverGuestFileData);
ftruncate($serverGuestFile, 0); // 清空原文件
rewind($serverGuestFile); // 把指针移回文件开头
fwrite($serverGuestFile, $serverGuestFileData);

// 释放锁并关闭文件
flock($serverGuestFile, LOCK_UN);
fclose($serverGuestFile);

核心改进点

  • LOCK_EX排他锁强制同一时间仅一个请求操作文件;
  • 把打开、读取、写入、关闭流程合并,锁的持有时间更短,减少等待;
  • ftruncate清空文件而非重新打开w模式,避免并发下的文件打开冲突。

方案2:使用临时文件写入(更安全)

如果担心锁机制在某些服务器环境下有兼容问题,可以用临时文件原子替换的方式:

$filePath = $_SERVER['DOCUMENT_ROOT'] . '/server/guest.txt';
$tempPath = $_SERVER['DOCUMENT_ROOT'] . '/server/guest.tmp';

// 读取原文件(加共享读锁,允许多个请求同时读取)
$serverGuestFile = fopen($filePath, "r");
flock($serverGuestFile, LOCK_SH);
$serverGuestFileData = fread($serverGuestFile, filesize($filePath));
flock($serverGuestFile, LOCK_UN);
fclose($serverGuestFile);

// 处理访客数据逻辑
$serverGuestFileData = json_decode($serverGuestFileData, true);
$_SESSION['guestID'] = array_search($ipAddress, array_column($serverGuestFileData, 0));
if ($_SESSION['guestID'] === false) {
    array_push($serverGuestFileData, array($ipAddress, $time));
    $_SESSION['guestID'] = sizeof($serverGuestFileData) - 1;
} else {
    $serverGuestFileData[$_SESSION['guestID']][1] = $time;
}

// 先写入临时文件
$serverGuestFileData = json_encode($serverGuestFileData);
$tempFile = fopen($tempPath, "w");
fwrite($tempFile, $serverGuestFileData);
fclose($tempFile);

// 原子替换原文件:这个操作在系统层面是原子性的,完全避免拼接问题
rename($tempPath, $filePath);

核心优势rename操作在绝大多数操作系统里是原子性的——要么替换成功,要么失败,不会出现内容拼接的中间状态,从根源上杜绝了文件损坏。

方案3:改用数据库存储(长期最优解)

如果你的网站访问量持续增长,用文本文件存储始终是隐患,建议把访客数据迁移到数据库:

  • SQLite不需要额外安装服务,配置简单,适合小流量网站;
  • 数据库本身有完善的事务和锁机制,完美解决并发问题;
  • 后续扩展功能(比如统计访客时长、过滤恶意IP)也会更方便。

举个SQLite的简单实现:

// 连接SQLite数据库(不存在则自动创建)
$db = new SQLite3($_SERVER['DOCUMENT_ROOT'] . '/server/guest.db');
// 初始化表(第一次运行时自动创建)
$db->exec("CREATE TABLE IF NOT EXISTS guests (ip TEXT PRIMARY KEY, last_active INTEGER)");

// 自动处理新增或更新:用REPLACE语句,IP存在则更新时间,不存在则新增
$stmt = $db->prepare("REPLACE INTO guests (ip, last_active) VALUES (:ip, :time)");
$stmt->bindValue(':ip', $ipAddress, SQLITE3_TEXT);
$stmt->bindValue(':time', $time, SQLITE3_INTEGER);
$stmt->execute();

// 获取guestID(用数据库的rowid作为唯一标识)
$stmt = $db->prepare("SELECT rowid FROM guests WHERE ip = :ip");
$stmt->bindValue(':ip', $ipAddress, SQLITE3_TEXT);
$result = $stmt->execute();
$row = $result->fetchArray();
$_SESSION['guestID'] = $row['rowid'];

$db->close();

额外防护措施

  1. 添加JSON格式校验:每次读取文件后,先检查json_decode的结果,如果返回null,说明文件损坏,此时可以初始化空数组或用备份恢复:
$serverGuestFileData = json_decode($serverGuestFileData, true);
if ($serverGuestFileData === null) {
    // 文件损坏,初始化空数组
    $serverGuestFileData = [];
    // 可选:记录错误日志,方便排查
    error_log("guest.txt 已损坏,已重置为空数组");
}
  1. 定期备份文件:比如每次写入前,把原文件复制一份作为备份,万一损坏可以快速恢复;
  2. 限制写入频率:同一个IP在短时间内不需要频繁更新last_active时间,比如判断如果距离上次更新不到1分钟,就跳过写入,减少并发写入的概率。

按照上面的方案调整后,就能完全解决你遇到的文件损坏问题了,有其他细节问题可以随时问我~

火山引擎 最新活动