客户端软件 — 用户统计方案

概述

本方案通过在客户端启动时上报计算机唯一标识至服务器,实现用户数量的统计。整体设计简洁轻量:客户端仅需发起一次 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');"

四、踩坑记录

  1. URLDownloadToFile 不适合 API 请求 --- 该函数主要用于下载文件,对于纯 API GET 请求,可能因缓存等原因导致请求无法到达服务器。改用 InternetOpen + InternetOpenUrl 方案后问题得以解决。

  2. GUID 花括号问题 --- StringFromGUID2 返回的 GUID 格式为 {xxx},其中的花括号在 URL 中可能引发问题,需通过 Remove 方法将其移除。

  3. WinINet 缓存机制 --- 相同的 URL 会被 WinINet 缓存,导致后续请求无法发送至服务器。通过添加 &t= 随机参数并设置 INTERNET_FLAG_RELOAD 标志来解决此问题。

  4. SQLite 目录写权限 --- 仅数据库文件具备写权限是不够的,SQLite 在执行写操作时会在同目录下创建 -journal 临时文件,因此目录也需要设置为 777 权限。

  5. 避免全局引入 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"
相关推荐
会编程的土豆2 小时前
Go 模板初识
linux·运维·服务器
腾讯蓝鲸智云2 小时前
标杆客户实践提炼:CFlow 版本价值流模板实战指南
运维·服务器·自动化·云计算·devops
阿标的博客2 小时前
Active Directory 组织单位和用户
服务器
再卷也是菜3 小时前
C++17(下)
c++
Deryck_德瑞克3 小时前
【Nginx】配置差异分析
服务器·前端·nginx
alexwang2113 小时前
HDU 4348 详细题解
c++·算法·题解·hdu·主席树·可持久化数据结构·可持久化线段树
前端 贾公子3 小时前
第06章:结构化输出 (上)
java·服务器·前端
程序员zgh3 小时前
C++ 拷贝赋值运算符 详解
c语言·开发语言·c++
一只小菜鸡..4 小时前
南京大学 操作系统 (JYY) 学习笔记:持久化的黑魔法——从磁铁、刻坑到闪存电荷
服务器·笔记·学习