
一、整体目标搭建一个本地可运行的热更测试环境实现生成资源 → 计算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 静态服务器方式 APython 一行命令最快# 进入资源根目录cdres_root# Python3 启动静态服务器端口8080python-mhttp.server8080# 访问测试http://localhost:8080/1.0.0/manifest.json方式 BNode.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)# 切换到资源目录serverHTTPServer((0.0.0.0,8080),HotUpdateHandler)print(热更服务器启动: http://localhost:8080)server.serve_forever()python server.py四、第二步编写清单生成脚本# gen_manifest.pyimportosimporthashlibimportjsonimportsysdefcalc_md5(filepath):计算文件MD5md5hashlib.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:iffilemanifest.json:continue# 跳过清单自身filepathos.path.join(root,file)rel_pathos.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}outputos.path.join(version_dir,manifest.json)withopen(output,w,encodingutf-8)asf:json.dump(manifest,f,indent2,ensure_asciiFalse)print(f✅ 清单已生成:{output})print(json.dumps(manifest,indent2,ensure_asciiFalse))if__name____main__:# 用法: python gen_manifest.py 1.0.1versionsys.argv[1]iflen(sys.argv)1else1.0.0base_urlhttp://localhost:8080version_diros.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 SERVERhttp://localhost:8080CACHE_DIRclient_cachedefhttp_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_okTrue)urllib.request.urlretrieve(url,save_path)defcalc_md5(filepath):md5hashlib.md5()withopen(filepath,rb)asf:forchunkiniter(lambda:f.read(4096),b):md5.update(chunk)returnmd5.hexdigest()defcheck_update():# 1. 获取服务器版本入口version_infohttp_get_json(f{SERVER}/version.json)res_versionversion_info[current_res_version]print(f 服务器资源版本:{res_version})# 2. 获取该版本清单remote_manifesthttp_get_json(f{SERVER}/{res_version}/manifest.json)# 3. 本地缓存目录按版本隔离local_diros.path.join(CACHE_DIR,res_version)# 4. 逐个比对下载need_download[]forresinremote_manifest[resources]:local_fileos.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_fileos.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# 制造测试资源echohero_v1res_root/1.0.0/hero_ui.bundleechoconfig_v1res_root/1.0.0/config.bytesechohero_v2_new_featureres_root/1.0.1/hero_ui.bundleechoconfig_v2res_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哈希判断是否命中灰度hint(hashlib.md5(str(uid).encode()).hexdigest(),16)return(h%100)percent# 命中灰度用 gray_version否则用 stable_version2. 断点续传下载defdownload_with_resume(url,save_path):resume_posos.path.getsize(save_path)ifos.path.exists(save_path)else0requrllib.request.Request(url)req.add_header(Range,fbytes{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.serverNginx / CDN阿里云OSS、七牛、腾讯云COS手动改 version.json运营后台可视化操作明文资源资源加密 签名校验单机测试多CDN节点 缓存刷新无监控崩溃率/成功率监控 自动回滚快速开始复制即用# 1. 创建目录和资源mkdir-pres_root/1.0.0 res_root/1.0.1echov1res_root/1.0.0/test.bundleechov2res_root/1.0.1/test.bundle# 2. 生成清单用上面的 gen_manifest.pypython 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