概述
本方案通过在客户端启动时上报计算机唯一标识至服务器,实现用户数量的统计。整体设计简洁轻量:客户端仅需发起一次 HTTP GET 请求,服务端则使用 PHP + SQLite3 存储数据。
一、客户端
1. 生成/读取计算机唯一 ID
采用 Windows 注册表存储 GUID,首次运行时生成并持久化,后续启动时直接读取。
cpp
CString GetOrCreateComputerID()
{
CString id;
HKEY hKey;
// 尝试读取已存在的 ID
if (RegOpenKeyEx(HKEY_CURRENT_USER, L"Software\\GudianShuwu", 0, KEY_READ, &hKey) == ERROR_SUCCESS)
{
WCHAR buf[64] = {0};
DWORD len = sizeof(buf);
if (RegQueryValueEx(hKey, L"ComputerID", NULL, NULL, (LPBYTE)buf, &len) == ERROR_SUCCESS)
id = buf;
RegCloseKey(hKey);
}
// 若不存在则创建新的 GUID
if (id.IsEmpty())
{
GUID guid;
if (SUCCEEDED(CoCreateGuid(&guid)))
{
WCHAR buf[64];
StringFromGUID2(guid, buf, 64);
id = buf;
if (RegCreateKeyEx(HKEY_CURRENT_USER, L"Software\\GudianShuwu", 0, NULL, 0, KEY_WRITE, NULL, &hKey, NULL) == ERROR_SUCCESS)
{
RegSetValueEx(hKey, L"ComputerID", 0, REG_SZ, (LPBYTE)(LPCWSTR)id, (id.GetLength() + 1) * sizeof(WCHAR));
RegCloseKey(hKey);
}
}
}
return id;
}
2. 启动时上报
程序启动时,通过 WinINet 向服务器发送一次 HTTP GET 请求。注意需移除 GUID 中的花括号 {},以避免 URL 编码问题。
cpp
// 统计用户数(上报计算机ID到服务器)
{
CString computerID = GetOrCreateComputerID();
if (!computerID.IsEmpty())
{
computerID.Remove(L'{');
computerID.Remove(L'}');
CString pingUrl;
pingUrl.Format(L"http://gudianxiaoshuo.com/api/ping/index.php?id=%s&t=%u",
computerID, GetTickCount());
HINTERNET hSession = InternetOpen(_T("GudianShuwu/1.0"),
INTERNET_OPEN_TYPE_PRECONFIG, NULL, NULL, 0);
if (hSession)
{
HINTERNET hUrl = InternetOpenUrl(hSession, pingUrl, NULL, 0,
INTERNET_FLAG_RELOAD | INTERNET_FLAG_NO_CACHE_WRITE, 0);
if (hUrl)
{
char buf[64];
DWORD dwRead = 0;
InternetReadFile(hUrl, buf, sizeof(buf), &dwRead);
InternetCloseHandle(hUrl);
}
InternetCloseHandle(hSession);
}
}
}
关键设计点:
| 设计 | 说明 |
|---|---|
| 注册表持久化 | GUID 存储在 HKEY_CURRENT_USER\Software\GudianShuwu\ComputerID |
| 去掉花括号 | StringFromGUID2 生成 {xxx} 格式,URL 中需移除 |
&t=随机数 |
添加随机参数以绕过 WinINet 缓存,确保每次请求都能真正发出 |
INTERNET_FLAG_RELOAD |
强制重新加载,不读取缓存 |
INTERNET_FLAG_NO_CACHE_WRITE |
禁止将响应写入本地缓存 |
3. 需要的头文件和库
在 MainFrm.cpp 中添加以下内容:
cpp
#include <wininet.h>
#pragma comment(lib, "wininet.lib")
二、服务器端
1. 环境
- 阿里云 ECS(Nginx + PHP 7.0)
- 文件路径:
/var/ftp/ttt/gudianxiaoshuo/api/ping/index.php - 数据库:
/var/ftp/ttt/gudianxiaoshuo/api/pings.db
2. PHP 接口
php
<?php
$id = isset($_GET['id']) ? $_GET['id'] : '';
if (!$id) { echo 'no id'; exit; }
$db = new SQLite3(dirname(__FILE__) . '/../pings.db');
$db->exec("CREATE TABLE IF NOT EXISTS pings (
id TEXT PRIMARY KEY,
time INTEGER,
ip TEXT
)");
$ip = isset($_SERVER['REMOTE_ADDR']) ? $_SERVER['REMOTE_ADDR'] : 'cli';
$stmt = $db->prepare('INSERT OR REPLACE INTO pings (id, time, ip)
VALUES (:id, :time, :ip)');
$stmt->bindValue(':id', $id, SQLITE3_TEXT);
$stmt->bindValue(':time', time(), SQLITE3_INTEGER);
$stmt->bindValue(':ip', $ip, SQLITE3_TEXT);
$stmt->execute();
echo 'ok';
数据库表结构:
| 字段 | 类型 | 说明 |
|---|---|---|
| id | TEXT (主键) | 计算机 GUID |
| time | INTEGER | 最近一次上报的 Unix 时间戳 |
| ip | TEXT | 最近一次上报的 IP 地址 |
3. 权限配置
SQLite 写入操作会在目录中创建 journal 临时文件,因此目录 和数据库文件均需具备写权限:
bash
chmod 777 /var/ftp/ttt/gudianxiaoshuo/api/
chmod 666 /var/ftp/ttt/gudianxiaoshuo/api/pings.db
三、数据库查询
查看所有记录
bash
sqlite3 -line /var/ftp/ttt/gudianxiaoshuo/api/pings.db \
"SELECT id, datetime(time, 'unixepoch', 'localtime'), ip FROM pings;"
统计总安装数
bash
sqlite3 /var/ftp/ttt/gudianxiaoshuo/api/pings.db \
"SELECT count(*) FROM pings;"
统计当天活跃用户
bash
sqlite3 /var/ftp/ttt/gudianxiaoshuo/api/pings.db \
"SELECT count(*) FROM pings
WHERE date(time, 'unixepoch', 'localtime') = date('now', 'localtime');"
四、踩坑记录
-
URLDownloadToFile 不适合 API 请求 --- 该函数主要用于下载文件,对于纯 API GET 请求,可能因缓存等原因导致请求无法到达服务器。改用
InternetOpen+InternetOpenUrl方案后问题得以解决。 -
GUID 花括号问题 ---
StringFromGUID2返回的 GUID 格式为{xxx},其中的花括号在 URL 中可能引发问题,需通过Remove方法将其移除。 -
WinINet 缓存机制 --- 相同的 URL 会被 WinINet 缓存,导致后续请求无法发送至服务器。通过添加
&t=随机参数并设置INTERNET_FLAG_RELOAD标志来解决此问题。 -
SQLite 目录写权限 --- 仅数据库文件具备写权限是不够的,SQLite 在执行写操作时会在同目录下创建
-journal临时文件,因此目录也需要设置为777权限。 -
避免全局引入 wininet.h --- 若将其放在
stdafx.h中,可能会影响其他已使用窄字符串调用 WinINet 的文件(例如TimXmlRpc.cpp)。建议仅在需要的.cpp文件中引入。
五、数据流
客户端启动
│
▼
GetOrCreateComputerID()
读取注册表 GUID
│
▼
InternetOpenUrl()
GET /api/ping/index.php?id=xxx&t=rand
│
▼
Nginx → PHP → SQLite3
INSERT OR REPLACE INTO pings
│
▼
返回 "ok"