游戏热更测试环境快速搭建指南
一、整体目标
搭建一个本地可运行的热更测试环境,实现:
生成资源 → 计算MD5 → 生成清单 → 起HTTP服务 → 客户端拉取更新 → 验证回滚二、目录结构规划
hotupdate-server/ ├── server.py # 简易HTTP服务器 ├── gen_manifest.py # 清单生成脚本 ├── res_root/ # 资源根目录(CDN模拟) │ ├── version.json # 版本控制入口(指针) │ ├── 1.0.0/ # 版本目录 │ │ ├── manifest.json │ │ ├── hero_ui.bundle │ │ └── config.bytes │ └── 1.0.1/ │ ├── manifest.json │ ├── hero_ui.bundle │ └── config.bytes └── client_cache/ # 模拟客户端本地缓存三、第一步:搭建 HTTP 静态服务器
方式 A:Python 一行命令(最快)
# 进入资源根目录cdres_root# Python3 启动静态服务器(端口8080)python-mhttp.server8080# 访问测试:http://localhost:8080/1.0.0/manifest.json方式 B:Node.js(支持更灵活)
# 安装 http-servernpminstall-ghttp-server# 启动(-c-1 禁用缓存,方便测试回滚)http-server ./res_root-p8080-c-1--cors⚠️
-c-1禁用缓存非常重要,否则改了清单客户端拉到的还是旧的。
方式 C:自定义 Python 服务器(支持版本接口)
# server.pyfromhttp.serverimportHTTPServer,SimpleHTTPRequestHandlerimportosclassHotUpdateHandler(SimpleHTTPRequestHandler):defend_headers(self):# 禁用缓存,添加跨域self.send_header('Cache-Control','no-store, no-cache, must-revalidate')self.send_header('Access-Control-Allow-Origin','*')super().end_headers()if__name__=='__main__':os.chdir('res_root')# 切换到资源目录server=HTTPServer(('0.0.0.0',8080),HotUpdateHandler)print('热更服务器启动: http://localhost:8080')server.serve_forever()python server.py四、第二步:编写清单生成脚本
# gen_manifest.pyimportosimporthashlibimportjsonimportsysdefcalc_md5(filepath):"""计算文件MD5"""md5=hashlib.md5()withopen(filepath,'rb')asf:forchunkiniter(lambda:f.read(4096),b''):md5.update(chunk)returnmd5.hexdigest()defgen_manifest(version_dir,version,base_url):"""扫描版本目录生成清单"""resources=[]forroot,_,filesinos.walk(version_dir):forfileinfiles:iffile=='manifest.json':continue# 跳过清单自身filepath=os.path.join(root,file)rel_path=os.path.relpath(filepath,version_dir)resources.append({"name":rel_path.replace('\\','/'),"md5":calc_md5(filepath),"size":os.path.getsize(filepath),"url":f"{base_url}/{version}/{rel_path}".replace('\\','/')})manifest={"version":version,"resources":resources}output=os.path.join(version_dir,'manifest.json')withopen(output,'w',encoding='utf-8')asf:json.dump(manifest,f,indent=2,ensure_ascii=False)print(f"✅ 清单已生成:{output}")print(json.dumps(manifest,indent=2,ensure_ascii=False))if__name__=='__main__':# 用法: python gen_manifest.py 1.0.1version=sys.argv[1]iflen(sys.argv)>1else'1.0.0'base_url='http://localhost:8080'version_dir=os.path.join('res_root',version)gen_manifest(version_dir,version,base_url)使用:
python gen_manifest.py1.0.0 python gen_manifest.py1.0.1五、第三步:版本控制入口(回滚开关)
// res_root/version.json —— 控制客户端拉哪个版本{"app_version":"1.0.0","current_res_version":"1.0.1","min_res_version":"1.0.0","force_update":false,"gray":{"enabled":false,"percent":10,"gray_version":"1.0.1","stable_version":"1.0.0"}}🎯回滚操作 = 把
current_res_version改回1.0.0,客户端下次启动即回退。
六、第四步:模拟客户端更新逻辑
# client.py —— 模拟客户端热更流程importosimportjsonimporthashlibimporturllib.request SERVER='http://localhost:8080'CACHE_DIR='client_cache'defhttp_get_json(url):withurllib.request.urlopen(url)asresp:returnjson.loads(resp.read().decode())defdownload(url,save_path):os.makedirs(os.path.dirname(save_path),exist_ok=True)urllib.request.urlretrieve(url,save_path)defcalc_md5(filepath):md5=hashlib.md5()withopen(filepath,'rb')asf:forchunkiniter(lambda:f.read(4096),b''):md5.update(chunk)returnmd5.hexdigest()defcheck_update():# 1. 获取服务器版本入口version_info=http_get_json(f'{SERVER}/version.json')res_version=version_info['current_res_version']print(f"🔍 服务器资源版本:{res_version}")# 2. 获取该版本清单remote_manifest=http_get_json(f'{SERVER}/{res_version}/manifest.json')# 3. 本地缓存目录(按版本隔离)local_dir=os.path.join(CACHE_DIR,res_version)# 4. 逐个比对下载need_download=[]forresinremote_manifest['resources']:local_file=os.path.join(local_dir,res['name'])ifnotos.path.exists(local_file)orcalc_md5(local_file)!=res['md5']:need_download.append(res)ifnotneed_download:print("✅ 已是最新,无需更新")returnprint(f"📥 需下载{len(need_download)}个文件")forresinneed_download:local_file=os.path.join(local_dir,res['name'])print(f" 下载{res['name']}...")download(res['url'],local_file)# 5. 下载后校验MD5ifcalc_md5(local_file)==res['md5']:print(f" ✅{res['name']}校验通过")else:print(f" ❌{res['name']}校验失败!")os.remove(local_file)print(f"🎉 更新完成,当前版本:{res_version}")if__name__=='__main__':check_update()运行:
python client.py七、完整测试流程(含回滚验证)
Step 1:准备资源
mkdir-pres_root/1.0.0 res_root/1.0.1# 制造测试资源echo"hero_v1">res_root/1.0.0/hero_ui.bundleecho"config_v1">res_root/1.0.0/config.bytesecho"hero_v2_new_feature">res_root/1.0.1/hero_ui.bundleecho"config_v2">res_root/1.0.1/config.bytesStep 2:生成清单
python gen_manifest.py1.0.0 python gen_manifest.py1.0.1Step 3:配置版本入口
// res_root/version.json 设为 1.0.1{"app_version":"1.0.0","current_res_version":"1.0.1"}Step 4:启动服务 + 客户端更新
# 终端1python server.py# 终端2python client.py# 输出:下载 1.0.1 资源,校验通过Step 5:🔥 模拟回滚
// 修改 version.json 指回 1.0.0{"app_version":"1.0.0","current_res_version":"1.0.0"}# 再次运行客户端python client.py# 输出:拉取 1.0.0 清单,回退到旧版本资源# 由于版本目录隔离,1.0.0 资源可能已缓存,秒回滚八、测试要点检查表
| 测试项 | 预期结果 | 验证方法 |
|---|---|---|
| 增量更新 | 只下载变化的文件 | 改一个文件重新生成清单,看是否只下1个 |
| MD5校验 | 损坏文件被拒绝 | 手动改坏本地文件,看是否重新下载 |
| 版本回滚 | 秒切回旧版本 | 修改 version.json,客户端重新拉取 |
| 缓存隔离 | 各版本独立存放 | 检查 client_cache 目录结构 |
| 断点续传 | 大文件中断可续 | (进阶)用 Range 请求实现 |
| 强更判断 | 包版本不符提示更新 | 修改 app_version 测试 |
九、进阶:加分功能
1. 灰度下发(按 UID 哈希)
defis_gray_user(uid,percent):"""根据UID哈希判断是否命中灰度"""h=int(hashlib.md5(str(uid).encode()).hexdigest(),16)return(h%100)<percent# 命中灰度用 gray_version,否则用 stable_version2. 断点续传下载
defdownload_with_resume(url,save_path):resume_pos=os.path.getsize(save_path)ifos.path.exists(save_path)else0req=urllib.request.Request(url)req.add_header('Range',f'bytes={resume_pos}-')withurllib.request.urlopen(req)asresp:withopen(save_path,'ab')asf:f.write(resp.read())3. 差分包(bsdiff)
# 生成差分包(只传变化的二进制部分)bsdiff old.bundle new.bundle patch.file# 客户端用旧文件 + patch 还原新文件bspatch old.bundle new.bundle patch.file十、生产环境升级建议
| 测试环境 | 生产环境 |
|---|---|
| Python http.server | Nginx / CDN(阿里云OSS、七牛、腾讯云COS) |
| 手动改 version.json | 运营后台可视化操作 |
| 明文资源 | 资源加密 + 签名校验 |
| 单机测试 | 多CDN节点 + 缓存刷新 |
| 无监控 | 崩溃率/成功率监控 + 自动回滚 |
快速开始(复制即用)
# 1. 创建目录和资源mkdir-pres_root/1.0.0 res_root/1.0.1echo"v1">res_root/1.0.0/test.bundleecho"v2">res_root/1.0.1/test.bundle# 2. 生成清单(用上面的 gen_manifest.py)python gen_manifest.py1.0.0 python gen_manifest.py1.0.1# 3. 配置版本入口echo'{"app_version":"1.0.0","current_res_version":"1.0.1"}'>res_root/version.json# 4. 启动服务器python server.py# 5. 另开终端运行客户端python client.py