由于项目的需求的变动,客户想要把原来由 javaEE 开发的 B/S 架构一个系统平台换为 C/S 架构的,考虑到项目进度和效率的问题,项目组决定采用 C# 的 winform 来实现客户端的开发,而服务器端直接引用原有的系统业务。考虑到客户端软件可能以后会不断地需要更新,因此做了一个软件自动更新的功能。闲话少说,转到正题!
首先我先要介绍一下该功能的总体实现思路:
首先考虑的是在服务端要有哪些方法来实现软件的更新功能呢?
一、软件需要更新,必然涉及到文件的读取操作,因此我们要有一个读取文件的方法;
二、软件更新的过程中需要用进度条来展示更新的进度,因此我们服务端还需要有一个获取文件大小的方法;
三、这是最重要的一点,就是客户端该如何来确认是否需要更新,更新那些文件?因此我们需要用一个 xml 文件来描述这些信息。
其次要考虑一下客户端的实现方式了,客户端应该如何实现呢?
一、 客户端首先要判断软件是否需要更新,要更新那些文件,因此我们必须先要把服务器上对软件更新的 xml 描述文件先从服务端下载下来,然后与客户端上的 xml 文件进行比较,看是否需要更新;
二、 若通过 xml 文件比较后,发现需要更新后,读取 xml 文件中需要更新的文件列表,然后依次下载需要更新的文件到临时的更新文件夹;
三、 停止主程序进程,替换掉程序中原有的文件,最后关闭更新程序,启动主程序,更新完成!
实现程序更新的效果图:
现在我们就根据我们的总体实现思路来一步一步完成该应用的实现:
一、
WebService
的开发源码
根据上面的思路我们分析出实现该应用我们至少需要两个方法,一个是读取文件的方法,一个是获取文件大小的方法,本人采用的是
JAX-WS 2.1
来实现
WebService
的,采用其他的服务类库也可以,只要实现该服务就可以了,我的服务实现类如下:
package com.updatesoft.service; import java.io.ByteArrayOutputStream; import java.io.File; import java.io.FileInputStream; import java.io.IOException; import java.net.URLDecoder; /** * 更新软件操作类 * @author jin * */ public class UpdateSoft { /** * 获取文件大小 * @param fileName 文件名称 * @return 文件大小(字节) */ public long getFileSize(String fileName) { int nFileLength = -1; try { String str = URLDecoder.decode(getClass().getClassLoader().getResource("com").toString(),"UTF-8"); str= str.substring(0, str.indexOf("WEB-INF/classes")); str=str.substring(6); System.out.println("路径:" + str); File file = new File(str + fileName); if (file.exists()) { FileInputStream fis = null; fis = new FileInputStream(file); nFileLength = fis.available(); } else { System.out.println("文件不存在"); } }catch (IOException e) { e.printStackTrace(); }catch (Exception e) { e.printStackTrace(); } System.out.println(nFileLength); return nFileLength; } /** * 根据偏移量和字节缓存大小分段获取文件字节数组 * @param fileName 文件名称 * @param offset 字节偏移量 * @param bufferSize 字节缓存大小 * @return 文件字节数组 */ public byte[] getUpdateFile(String fileName, int offset, int bufferSize) { byte[] ret = null; try { String str = URLDecoder.decode(getClass().getClassLoader().getResource("com").toString(),"UTF-8"); str= str.substring(0, str.indexOf("WEB-INF/classes")); str=str.substring(6); File file = new File(str + fileName); if (!file.exists()) { return null; } FileInputStream in = new FileInputStream(file); ByteArrayOutputStream out = new ByteArrayOutputStream(1024); byte[] b = new byte[1024]; int n; int t = 0; while ((n = in.read(b)) != -1) { if(t >= offset && t< offset + bufferSize){ out.write(b, 0, n); } t += n; } in.close(); out.close(); ret = out.toByteArray(); } catch (IOException e) { e.printStackTrace(); } return ret; } }
客户端所需要调用的服务方法我们已经实现了,接下来我们需要准备我们软件更新的资源了(即需要更新的文件和更新文件的描述文件 update.xml )。资源文件根据需求上传到服务器中,其中 update.xml 文件格式如下:
<?xml version="1.0" encoding="UTF-8"?> <update> <forceUpdate>false</forceUpdate> <version>20100812</version> <subversion>1</subversion> <filelist count="5"> <file name="music/陈瑞 - 白狐.mp3">true</file> <file name="music/韩红 - 擦肩而过.mp3">true</file> <file name="music/林俊杰 - 背对背拥抱.mp3">true</file> <file name="music/油菜花-成龙.mp3">true</file> <file name="music/郑智化 - 别哭我最爱的人.mp3">true</file> </filelist> <executeFile>SystemUpdateClient.exe</executeFile> </update>
根节点为 update , forceUpdate 为是否强制更新, ture 则为是, false 则为否; version 为主版本号, subversion 为次版本号, flielist 为需要更新的文件列表,属性 count 指定需要更新的文件数, flie 为文件节点, name 属性指定文件名称,值 true 为需要更新,值 false 为不需要更新。 executeFile 指定软件更新完成后需要重新启动的可执行文件。
二、 客户端的开发源码
客户端的实现也比较简单,本人采用的是 vs2008 的开发工具,在解决方案中新建一个软件更新的窗体,在窗体中拖入一个文本框和两个进度条,文本框用于显示更新过程,两个进度条一个用于显示总进度,一个显示单个文件进度。为了 解决多线程环境中跨线程改写 ui 控件属性问题,我这里采用了代理方法,实现代码如下:
using System; using System.Collections.Generic; using System.ComponentModel; using System.Data; using System.Drawing; using System.Linq; using System.Text; using System.Windows.Forms; using System.Threading; using System.IO; using System.Xml; namespace SystemUpdateClient { public partial class update : Form { /// <summary> /// 每次下载并写入磁盘的文件数据大小(字节) /// </summary> private static int BUFFER_SIZE = 15 * 1024; //把窗体改为单例模型 private static update updateForm; public static update getUpdateForm() { if (updateForm == null) { updateForm = new update(); } return updateForm; } //构造函数改为私有,外部程序不可以使用 new() 来创建新窗体,保证了窗体唯一性 private update() { InitializeComponent(); } //******** 定义代理方法,解决多线程环境中跨线程改写 ui 控件属性,开始 ******** //定义设置一个文本的委托方法(字符串) private delegate void setText(string log); //定义设置一个进度的委托方法(整型) private delegate void setProcess(int count); //设置总进度条的最大数 private void setProgressBar1_Maximum(int count) { progressBar1.Maximum = count; } //设置单文件进度条的最大数 private void setProgressBar2_Maximum(int count) { progressBar2.Maximum = count; } //设置总进度条的当前值 private void setProgressBar1_value(int count) { progressBar1.Value = count; } //设置单文件进度条当前值 private void setProgressBar2_value(int count) { progressBar2.Value = count; } //设置总文件进度条步进进度 private void addProgressBar1_value(int count) { if (progressBar1.Maximum > progressBar1.Value) { progressBar1.Value += count; } else { progressBar1.Value = progressBar1.Maximum; } } //设置单文件进度条步进进度 private void addProgressBar2_value(int count) { if (progressBar2.Maximum > progressBar2.Value) { progressBar2.Value += count; } else { progressBar2.Value = progressBar2.Maximum; } } //设置文本框的值 private void UpdateText(string log) { textBox1.Text += log; } //******** 定义代理方法,解决多线程环境中跨线程改写 ui 控件属性 结束 ******** /// <summary> /// 窗体显示时,调用 invokeThread 方法 /// </summary> /// <param name="sender"></param> /// <param name="e"></param> private void update_Shown(object sender, EventArgs e) { invokeThread(); } /// <summary> /// 开启一个线程,执行 update_function 方法 /// </summary> void invokeThread() { Thread th = new Thread(new ThreadStart(update_function)); th.Start(); } /// <summary> /// 自动更新方法,整合实现下面的业务逻辑。 /// </summary> private void update_function() { //判断 位于本地客户端程序文件夹 update 是否存在 if (Directory.Exists(Application.StartupPath + "/update")) { //存在则删除,true 表示移除包含的子目录及文件 Directory.Delete("update/", true); } try{ //通过 webservice 从服务器端获取更新脚本文件 update.xml getUpdateXMLFile(); } catch (Exception e) { MessageBox.Show("无法进行更新,访问服务器失败!\n\r原因:" + e.Message, "警告", MessageBoxButtons.OK, MessageBoxIcon.Warning); } //判断强制更新开关 if (isForceUpdate()) { //通过 webservice 从服务器端下载更新程序文件 downloadFiles(); } else { //比较版本号 if (verifyVersion()) { //通过 webservice 从服务器端下载更新程序文件 downloadFiles(); } } DialogResult result = MessageBox.Show("更新完成!", "提示", MessageBoxButtons.OK, MessageBoxIcon.Information); if (result == DialogResult.OK) { //启动客户端主程序,退出更新程序 appExit(); } } /// <summary> /// 下载 update.xml /// </summary> private void getUpdateXMLFile() { //执行委托方法,更新文本控件内容 textBox1.Invoke(new setText(this.UpdateText), new object[] { "正在从服务器下载 更新脚本文件 update.xml \r\n" }); //创建一个文件传送的 webservice 接口实例 updateservice.UpdateSoftDelegateClient sendFileWS = new updateservice.UpdateSoftDelegateClient(); //通过 webservice接口 获取服务器上 update.xml 文件的长度。 long fileSize = sendFileWS.getFileSize("update.xml"); //判断本地客户端文件夹下 update 目录是否存在 if (!Directory.Exists(Application.StartupPath + "/update")) { //不存在则创建 update 目录 Directory.CreateDirectory(Application.StartupPath + "/update"); } //通过定义文件缓冲区分块下载 update.xml 文件 for (int offset = 0; offset < fileSize; offset += BUFFER_SIZE) { //从服务器读取指定偏移值和指定长度的二进制文件字符数组 byte[] bytes = sendFileWS.getUpdateFile("update.xml", offset, BUFFER_SIZE); //如果 字符数组不为空 if (bytes != null) { //以追加方式打开 update.xml 文件 using (FileStream fs = new FileStream(Application.StartupPath + "/update/update.xml", FileMode.Append)) { //写入数据 fs.Write(bytes, 0, bytes.Length); fs.Close(); } } } } /// <summary> /// 是否开启强制更新。 /// </summary> /// <returns>true 开启强制更新,false 比较版本号后再更新</returns> private bool isForceUpdate() { try { //开始解析 update/update.xml 新文件 XmlDocument doc = new XmlDocument(); doc.Load("update/update.xml"); XmlElement root = doc.DocumentElement; //节点是否存在 if (root.SelectSingleNode("forceUpdate") != null) { //获取 forceUpdate 节点的内容 string forceUpdate = root.SelectSingleNode("forceUpdate").InnerText; doc = null; if (forceUpdate.Equals("true")) { textBox1.Invoke(new setText(this.UpdateText), new object[] { "强制更新开关已打开,不再匹配版本号。 \r\n" }); return true; } else { return false; } } else { doc = null; return false; } } catch { //发生异常,则更新程序,覆盖 update.xml MessageBox.Show("版本文件解析异常,服务器端 update.xml 可能已经损坏,请联系管理员。", "警告", MessageBoxButtons.OK, MessageBoxIcon.Warning); return true; } } /// <summary> /// 解析 update.xml 文件,比较version 和 subversion 判断是否有新版本 /// </summary> /// <returns>true 有新版本,false 版本相同</returns> private bool verifyVersion() { try { if (!File.Exists("update.xml")) { return true; } //开始解析 update.xml 旧文件 XmlDocument doc1 = new XmlDocument(); doc1.Load("update.xml"); XmlElement root1 = doc1.DocumentElement; //开始解析 update/update.xml 新文件 XmlDocument doc2 = new XmlDocument(); doc2.Load("update/update.xml"); XmlElement root2 = doc2.DocumentElement; if (root1.SelectSingleNode("version") != null && root1.SelectSingleNode("subversion") != null && root2.SelectSingleNode("version") != null && root2.SelectSingleNode("subversion") != null) { int old_version = Convert.ToInt32(root1.SelectSingleNode("version").InnerText); int old_subversion = Convert.ToInt32(root1.SelectSingleNode("subversion").InnerText); int new_version = Convert.ToInt32(root2.SelectSingleNode("version").InnerText); int new_subversion = Convert.ToInt32(root2.SelectSingleNode("subversion").InnerText); doc1 = null; doc2 = null; textBox1.Invoke(new setText(this.UpdateText), new object[] { "正在判断版本号...\r\n" }); //判断版本号和子版本号 if (old_version == new_version && old_subversion == new_subversion) { textBox1.Invoke(new setText(this.UpdateText), new object[] { "已经是最新版本,无需更新\r\n" }); return false; } else { textBox1.Invoke(new setText(this.UpdateText), new object[] { "发现新版本,开始读取更新列表 \r\n" }); return true; } } else { textBox1.Invoke(new setText(this.UpdateText), new object[] { "无法解析版本号,将下载更新全部文件...\r\n" }); doc1 = null; doc2 = null; return true; } } catch { //发生异常,则更新程序,覆盖 update.xml MessageBox.Show("版本文件解析异常,服务器端 update.xml 可能已经损坏,请联系管理员。", "警告", MessageBoxButtons.OK, MessageBoxIcon.Warning); return true; } } /// <summary> /// 解析 update.xml,下载更新文件 /// </summary> public void downloadFiles() { //解析 update.xml XmlDocument doc = new XmlDocument(); doc.Load("update/update.xml"); XmlElement root = doc.DocumentElement; XmlNode fileListNode = root.SelectSingleNode("filelist"); //获取更新文件的数量 int fileCount = Convert.ToInt32(fileListNode.Attributes["count"].Value); //调用委托方法,更新控件内容。 textBox1.Invoke(new setText(this.UpdateText), new object[] { "需更新文件数量 " + fileCount.ToString() + "\r\n" }); //结束 SystemUpdateClient.exe 进程 System.Diagnostics.Process[] processes = System.Diagnostics.Process.GetProcesses(); foreach (System.Diagnostics.Process process in processes) { if (process.ProcessName == "SystemUpdateClient.exe") { process.Close(); break; } } //总文件大小,用于设置总进度条最大值 long totalFileSize = 0; //循环文件列表,获取总文件的大小 for (int i = 0; i < fileCount; i++) { XmlNode itemNode = fileListNode.ChildNodes[i]; //获取更新文件名 string fileName = itemNode.Attributes["name"].Value; //获取需要更新文件的总大小,调用 webservice 接口 updateservice.UpdateSoftDelegateClient sendFileWS = new updateservice.UpdateSoftDelegateClient(); //获取文件长度(字节) long fileSize = sendFileWS.getFileSize(fileName); totalFileSize += fileSize; } //调用委托方法,设置总进度条的最大值。 progressBar1.Invoke(new setProcess(this.setProgressBar1_Maximum), new object[] { (int)(totalFileSize / BUFFER_SIZE) + 1 }); //调用委托方法,更新控件内容。 textBox1.Invoke(new setText(this.UpdateText), new object[] { "开始更新...\r\n" }); //循环文件列表 for (int i = 0; i < fileCount; i++) { XmlNode itemNode = fileListNode.ChildNodes[i]; //获取更新文件名 string fileName = itemNode.Attributes["name"].Value; //调用委托方法,更新控件内容。 textBox1.Invoke(new setText(this.UpdateText), new object[] { "正在下载文件 " + fileName + "\r\n" }); //分块下载文件,调用 webservice 接口 updateservice.UpdateSoftDelegateClient sendFileWS = new updateservice.UpdateSoftDelegateClient(); //获取文件长度(字节) long fileSize = sendFileWS.getFileSize(fileName); //调用委托方法,更新进度条控件内容。 progressBar2.Invoke(new setProcess(this.setProgressBar2_Maximum), new object[] { (int)(fileSize / BUFFER_SIZE) + 1 }); progressBar2.Invoke(new setProcess(this.setProgressBar2_value), new object[] { 0 }); //通过 webservice 接口 循环读取文件数据块,每次向前步进 BUFFER_SIZE for (int offset = 0; offset < fileSize; offset += BUFFER_SIZE) { Byte[] bytes = sendFileWS.getUpdateFile(fileName, offset, BUFFER_SIZE); if (bytes != null) { if (fileName.LastIndexOf("/") != 0) { string newpath = fileName.Substring(0, fileName.LastIndexOf("/")); if (!Directory.Exists(Application.StartupPath + "/update/" + newpath)) { //不存在则创建 update 目录 Directory.CreateDirectory(Application.StartupPath + "/update/" + newpath); } } //将下载的更新文件写入程序目录的 update 文件夹下 using (FileStream fs = new FileStream(Application.StartupPath + "/update/" + fileName, FileMode.Append)) { fs.Write(bytes, 0, bytes.Length); fs.Close(); } } bytes = null; progressBar2.Invoke(new setProcess(this.addProgressBar2_value), new object[] { 1 }); progressBar1.Invoke(new setProcess(this.addProgressBar1_value), new object[] { 1 }); } //替换文件 try { if (fileName.LastIndexOf("/") != 0) { string newpath = fileName.Substring(0, fileName.LastIndexOf("/")); if (!Directory.Exists(Application.StartupPath + "/" + newpath)) { //不存在则创建 update 目录 Directory.CreateDirectory(Application.StartupPath + "/" + newpath); } } if (fileName != "SystemUpdateClient.XmlSerializers.dll" || fileName != "SystemUpdateClient.exe.config" || fileName != "SystemUpdateClient.pdb" || fileName != "SystemUpdateClient.exe") { File.Copy("update/" + fileName, fileName, true); } } catch { textBox1.Invoke(new setText(this.UpdateText), new object[] { "无法复制" + fileName + "\r\n" }); } //progressBar1.Invoke(new setProcess(this.addProgressBar1_value), new object[] { 1 }); } textBox1.Invoke(new setText(this.UpdateText), new object[] { "更新完成,更新程序正在做最后操作\r\n" }); //最后复制更新信息文件 File.Copy("update/update.xml", "update.xml", true); } /// <summary> /// 启动客户端主程序,退出更新程序 /// </summary> private void appExit() { //判断 位于本地客户端程序文件夹 update 是否存在 if (Directory.Exists(Application.StartupPath + "/update")) { //存在则删除,true 表示移除包含的子目录及文件 Directory.Delete("update/", true); } //获取主程序执行文件名 XmlDocument doc = new XmlDocument(); doc.Load("update.xml"); XmlElement root = doc.DocumentElement; string executeFile = string.Empty; //节点是否存在 if (root.SelectSingleNode("executeFile") != null) { //获取 executeFile 节点的内容 executeFile = root.SelectSingleNode("executeFile").InnerText; } doc = null; //启动客户端程序 System.Diagnostics.Process.Start(Application.StartupPath + @"\" + executeFile); //更新程序退出 Application.Exit(); } } }
通过源代码大家可以通过方法 update_function() 看出该应用的流程来,它首先是从服务端下载 update.xml 文件 ( 调用 getUpdateXMLFile()) ,根据下载的 xml 文件判断是否需要强制更新(调用 isForceUpdate() ),若是需要强制更新,那么将会强制更新所有的文件(调用 downloadFiles() ),若不需要强制更新则比较版本号(调用 verifyVersion() ),若版本号不同,则更新客户端软件,执行更新操作(调用 downloadFiles() ),更新完成后退出更新程序,启动主程序的可执行文件(调用 appExit() )。
到此我们整个软件更新应用算是已经完成了,关于代码的具体含义,方法的执行内容,大家看一下代码就明白了,很好理解的!