桌面软件的智能更新
——MagicUpdate更新原理
作者:陈秋明
Email: qmroom#126.com [# = @]
测试环境:Win2000/XP/2003/Vista/Windows7
关键字:更新、多线程、断点续传、Ftp、Exe互嵌
MagicUpdate下载,请与本人联系
本文PDF下载
索引
1 摘要:
1.1 使用到的技术:
1.2 更新流程
1.3 模块划分
2 界面展示
3 网络及多线程技术
3.1 多线程下载和断点续传
3.2 Ftp上传下载
3.3 多线程与主线程(界面)的数据交互
4 更新细节
4.1 更新模式
4.2 文件结构设计
4.2.1 服务器端文件信息
4.2.2 客户端文件信息ClientConfig结构
4.2.3 管理端文件信息ManagerConfig结构
4.2.4 宏说明及文件路径的计算
4.3 更新详细流程
4.3.1 管理端流程
4.3.2 客户端流程
4.4 客户端命令行
4.5 自我升级
5 小结
随着桌面软件文件的增多增大、版本升级频繁,有时因为改动了几个很小的几个dll文件,客户不得不从网站上重新下载整个安装包。并且开发人员不得不从繁忙的工作中抽出大量的时间来维护版本的升级,导致开发成本增加、维护难度大等诸多问题。有没有更完美的解决方案,来让软件自动升级到新版本呢?答案当然是肯定的。在当今网络技术技术迅速发展的时代,各个软件厂商各有各自的升级方法,现在我就谈谈我的解决方案。
关键技术:
l 多线程管理及交互
l Http下载和断点续传
l Ftp上传下载
l Vc++对Xml读写解析
l Md5算法
l 可执行程序Exe的相互嵌套
控件技术:
l 自定义三态树形控件CTreeCtrl和CListCtrl的互动
l 自定义CListCtrl,实现控件互嵌(进度条、编辑框、下拉框)
l 控件的自绘制
1. 管理端上传当前版本的文件到服务器;
2. 客户端先自我更新,然后更新产品。获取服务器信息,判断是否需要更新。若需要则提示用户更新。
管理端:
客户端:
自动模式下的更新提示:
升级程序自我更新:
桌面软件的升级的方法有很多,如何自动的升级,当然少不了数据传输程序。为了能够穿透防火墙在广域网使用我采用了基于http的下载。
原理大致如下:
1. 创建一个的空文件,大小为需要下载的文件大小+零时数据大小;
2. 创建多个线程,同时利用CreateFile共享写打开临时文件,移动文件指针到数据块开始区域;
3. 使用WinNet API,建立多个服务器连接,向服务器请求同一个文件的不同数据段的数据保存到同一个临时文件,并保存已下载的数据大小;
4. 数据都读完后,修改文件大小为原来大小。
若下载中途网络中断,下次下载时,则可以从尾部文件信息中获取已下载的大小,便于继续上一次的下载。实现起来当然要考虑诸多细节方面,由于篇幅有限,就不详细说明。
利用WinNet函数很容易实现Ftp的上传下载,这里就不详细说明。
交互的实现很复杂,用到了指针,回调函数,自己的消息队列等。要全部讲清楚确不容易,我就简要介绍一下。
1. 定义消息结构
typedef struct _NotifyW3ctPara
{
int nIndex; //同一个下载文件中线程索引
UINT nNotityType; //消息类型
DWORD dwContent; //通过StartDownload参数传入的值
LPVOID lpNotifyData; //消息数据
W3CT *pThread; //下载模块指针
}NotifyW3ctPara;
2. 定义消息存储数组和回调函数
typedef CArray<NotifyW3ctPara, NotifyW3ctPara&> ArrayNotifyW3ctPara;
// 消息通知回调函数
typedef void (*FunPtr_NotifyW3ct)(NotifyW3ctPara *);
3. 处理消息
//添加消息
BOOL UpdateProcCtrl::Add_NotifyPara(NotifyW3ctPara *pNotifyPara)
{
…
NotifyW3ctPara notifypara = {0};
memcpy(¬ifypara, pNotifyPara, sizeof(NotifyW3ctPara));
m_csNotifyPara.Lock();
…
try{
m_aryNotifyW3ctPara.Add(notifypara);
…
}
catch(CMemoryException *e){
…
}
m_csNotifyPara.Unlock ();
…
}
//清空消息
void UpdateProcCtrl::Clear_NotifyPara()
{
m_csNotifyPara.Lock();
m_aryNotifyW3ctPara.RemoveAll();
m_csNotifyPara.Unlock();
}
//消息回调函数
void UpdateProcCtrl::Callback_Notify(NotifyW3ctPara *pNotifyPara)
{
AppSetting::g_UpdateProcCtrl.Add_NotifyPara(pNotifyPara);
::SendMessage(g_main_wnd->GetSafeHwnd(), WM_TRANSFER_NOTIFY, NULL, NULL);
}
//消息处理
void UpdateProcCtrl::Deal_Notify(NotifyW3ctPara *pNotifyPara)
{
…
switch(pNotifyPara->nNotityType)
{
case NTD_THREAD_TANSFER_SIZE:
…
//在这里计算下载的速度、进度等
break;
}
}
//消息传输
LRESULT UpdateProcCtrl::OnTransferNotify(WPARAM wParam, LPARAM lParam)
{
ArrayNotifyW3ctPara aryNotifyW3ctPara;
m_csNotifyPara.Lock();
aryNotifyW3ctPara.Append(m_aryNotifyW3ctPara);
m_aryNotifyW3ctPara.RemoveAll();
m_csNotifyPara.Unlock();
for ( int i=0; i<aryNotifyW3ctPara.GetSize(); i++ )
{
NotifyW3ctPara &NotifyPara = aryNotifyW3ctPara.GetAt(i);
Deal_Notify(&NotifyPara);
}
return TRUE;
}
这里的消息处理大致就是这样,中间传输考虑很多同步互斥。如果您对win的内核对象很了解,看这些代码相信不是什么难事,中间省略掉代码可以自己发挥了。
需要注意的是,多线程处理时线程间通信、交互以及取消线程都应该是安全的,不要使用TerminateThread, TerminateProcess等这些不安全的API,如果使用了他们,那么你的程序执行结果将是不可预料的。
更新模式分为MD5比较和文件版本号比较
MD5比较,即先计算文件的md5值,根据md5值判断文件是否变化;
版本号比较,即直接判断文件版本号是否是最新
文件信息分为三个部分:客户端信息、管理端信息、服务器端信息。服务器端信息又分为状态信息和文件信息。
服务器端宏说明
(?ServerDir) - 和服务器配置文件同一目录
(?ServerRoot) - 根目录
(?None) - 空
客户端宏说明
(?LocalFullPathName)- 本地绝对路径,ExecuteCommand中用
(?LocalDir) - 和本地配置文件同一目录
(?None) - 空
以后可能会添加支持系统环境变量
例如:如果服务器配置文件地址为:http://localhost/update/ServerConfig.xml,则
(?ServerDir) = http://localhost/update/,
(?ServerRoot) = http://localhost/
服务器端文件真实地址计算方法:
如果server_root="(?None)",则
如果server_root="(?ServerDir)",则(?ServerDir)/
如果server_root="(?ServerRoot)",则(?ServerRoot)/
支持的节点:ServerPathName,ExecuteCommand
本地路径计算方法类似。
支持的节点:LocalPathName
文件比较方法分为MD5法和版本号比较法。当为版本号比较时,将配置文件LocalPathName换成绝对路径,再比较版本号。
此时,文件路径中请不要包含"."和"..",否则比较路径会不正确。
用法: MagicUpdateWizard [/automation] [/normal] [/close:hWnd[,Msg[,wParam[,lParam]]]] [/background] [/debug]
[/s:[a][,ap1][,ap2][,ap3][,ae][,aep1][,aep2][,aep3][,fu][,fn][,fs][,fe][,ff][,fa][,hpa][hp1][,hp2][,hp3]]
[/noupdateself] [/r:exepath] [/h/?] [/f[:serchpath]]/mucpath
/automation - 自动执行更新,等同/s:fu,ap1,aep1,hp1,ap2,hp2,aep2;若没有更新会自动退出;显示更新进度
/normal - 正常模式,等同/s:ap1,aep1,hp1;隐藏页面1
/close:hWnd - 更新前发送关闭通知给窗口句柄 hWnd(十进制);Msg消息类型(十进制),默认WM_CLOSE;wParam,lParam消息参数
/background - 后台运行;等同/s:a,ae,hpa;若有更新将自动升级,没有更新自动退出;看不见窗口
/debug - 调试模式(包含/updatetest)
/updatetest - 更新服务器上的测试版本
/s: - 显示类型
a - 自动执行;,等同/s:ap1,ap2,ap3;但不会自动退出,有则更新
ap1 - 自动执行页面1
ap2 - 自动执行页面2
ap3 - 自动执行页面3
ae - 自动退出,等同/s:aep1,aep2,aep3;
aep1 - 自动退出页面1
aep2 - 自动退出页面2
aep3 - 自动退出页面3
fu - 有更新时,提示
fn - 没有更新时,提示
fs - 更新成功时,提示
fe - 更新失败时,提示
ff - 更新完成时提示时,提示;等同/s:fs,fe
fa - 提示全部;等同/s:fu,fn,ff
hpa - 隐藏所有页面;等同/s:hp1,hp2,hp3
hp1 - 隐藏页面1
hp2 - 隐藏页面2
hp3 - 隐藏页面3
/noupdateself - 不进行自我更新
/r - 快退出时,执行 exepath
/f - 自动搜索 serchpath 目录下的匹配的 muc 文件;若省略,则搜索工作路径
mucpath - muc 文件路径
/h/? - 显示本帮助
大家知道,程序在运行时,不能改写和删除自己。要改写自己最简单的方法就是创建一个新进程B,由新的进程B改写原来的进程A。因此我按照如下的步骤实现自我更新。
1. 新建一个空的Win32工程“EmbedUpdateSelf”。在WinMain函数中添加更改原程序的代码,做好接口调用;
2. 手动编辑原项目工程下的res/MagicUpdateWizard.rc2资源,以便在原程序中嵌入新的程序,在需要更新时,原程序从资源中读出数据并写到文件中。添加如下代码:
#ifdef _DEBUG
IDR_EMBEDUPDATESELF EmbedUpdateSelf "..//..//Bin//EmbedUpdateSelfd.exe"
#else
IDR_EMBEDUPDATESELF EmbedUpdateSelf "..//..//Bin//EmbedUpdateSelf.exe"
#endif
3. 在更新服务器上放置需要更新的ZIP压缩包;
4. 在A进程中判断是否需要更新,下载压缩包,启动进程B,由B进程解压缩ZIP,并负责A的更新;
5. 更新成功后,再次用A原来的命令行启动进程A;
6. 删除任何用的的临时文件。
更新程序的大致原理粗糙的讲述完,篇幅所限在实际运用中还有很多的细节没有呈上,如更新程序和用户程序的通信(通知退出)、更新程序在实际使用的命令行方法、注意点、配置的服务器等。
写到这里还是意犹未尽。试想我们是否可以实现像Window Update一样,具有通知区域及服务程序的自动执行呢?我们在下载过程中是否可以考虑P2P、迅雷、电驴技术,加快下载速度呢?我们是否可以考虑在服务器上安装服务程序,进行必要的处理呢?我们是否考虑过更新程序在其他平台(LINUX、苹果、PDA、塞班)上运行呢?……
留给我们的疑问太多太多,可以升级的空间也太大太大,技术的拓展永无止境!
朋友们如果有什么好想法或更好的程序,欢迎来信(qmroom#126.com # = @)交流!