??? 由于項目的需求的變動,客戶想要把原來由 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元等您想捐的金額吧,狠狠點擊下面給點支持吧,站長非常感激您!手機微信長按不能支付解決辦法:請將微信支付二維碼保存到相冊,切換到微信,然后點擊微信右上角掃一掃功能,選擇支付二維碼完成支付。
【本文對您有幫助就好】元

