??? 由于項目的需求的變動,客戶想要把原來由 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() )。
到此我們整個軟件更新應用算是已經完成了,關于代碼的具體含義,方法的執行內容,大家看一下代碼就明白了,很好理解的! ?
更多文章、技術交流、商務合作、聯系博主
微信掃碼或搜索:z360901061

微信掃一掃加我為好友
QQ號聯系: 360901061
您的支持是博主寫作最大的動力,如果您喜歡我的文章,感覺我的文章對您有幫助,請用微信掃描下面二維碼支持博主2元、5元、10元、20元等您想捐的金額吧,狠狠點擊下面給點支持吧,站長非常感激您!手機微信長按不能支付解決辦法:請將微信支付二維碼保存到相冊,切換到微信,然后點擊微信右上角掃一掃功能,選擇支付二維碼完成支付。
【本文對您有幫助就好】元
