一、前言
最近接手了个半CS半BS的项目。怎么说呢?由于项目比较紧张,而且BS的项目已经做出来了,虽说不是很好,但是也可以满足增删改查的操作。但是CS的项目比较紧,给了一个月的时间,如果每个功能都做的话,时间根本不够,就算时间够,资金也不够。所以就在CS的界面中调用了BS的界面,然后界面显示的是BS的信息。
但是CS存在一个问题啊!那就是更新啊?CS的软件肯定有更新的功能,所以在以后的更新过程中一定会有变化的。在这篇博客中,小编就说说软件更新。
二、说说更新
提到更新,最常见的无非分为两种:
- 更新改变的文件
- 下载最新的安装包,重新安装,但是要保留用户的相关信息
小编在这篇博客中着重介绍一下第一种,把改变的文件更新到服务器,,然后客户端运行后会自动检查是否存在更新,存在更新就把文件下载下来,同名的文件会被新的文件覆盖。
三、更新程序文件
3.1 思路图
日行千里,先找对方向。
解析:
在图中,分成了两个部分:服务器+客户端。服务器主要是用于存放系统更新的文件以及更新的xml文件。而客户端就是我们使用的程序,类似QQ。
当我们的服务器配置文件更新后,客户端检测到后,就会提示更新,显示更新的内容,然后开始下载内容,最后同步服务器和客户端的配置文件。确保是同一个版本。
3.2 更新环境搭建
3.2.1 程序搭建
对于更新的程序小编是把它取出来,作为一个独立的程序,当主程序运行的时候会检测是否存在更新。来调用更新程序编译好的exe文件。
3.2.2 服务器搭建
服务器的选择可以是iis,ftp,weblogic,tomcat等。小编这里选择的是iis和ftp,其他的服务器会在以后展示。具体搭建请参考:
【BS】Windwos server 2008 服务器安装 IIS
【B/S】IIS的配置以及发布网站
C# 之 FTP服务器中文件上传与下载(一)
解决IIS 不能下载.MP4.dat .lib .pdb .ini后缀文件的方法
3.3 检查更新,检查是否存在更新
判断条件:通过对比本地的xml文件中的总版本信息和服务器端的总版本信息是否相同。不相同则是存在更新,相同这是没有更新。
通过调用app.IsUpdate方法来判断
#region 检查是否存在更新-王雷-2017年4月13日16:58:50
/// <summary>
/// 检查是否存在更新-王雷-2017年4月13日16:58:50
/// </summary>
public static void checkUpdate()
{
//获得程序的exe文件路径
SoftUpdate app = new SoftUpdate(Application.ExecutablePath, "BlogWriter");
app.UpdateFinish += new UpdateState(app_UpdateFinish);
try
{
//判断是否要更新
if (app.IsUpdate && MessageBox.Show("检查到新版本,是否更新?", "Update", MessageBoxButtons.YesNo, MessageBoxIcon.Question) == DialogResult.Yes)
{
//如果要更新就打开更新的页面
FrmUpdate fUpdate = new FrmUpdate();
fUpdate.ShowDialog();
}
}
catch (Exception ex)
{
MessageBox.Show(ex.Message);
}
}
#endregion
在IsUpdate方法中会调用checkUpdate方法来检查:
#region 获取是否需要更新-王雷-2017年4月13日17:01:37
/// <summary>
/// 获取是否需要更新
/// </summary>
public bool IsUpdate
{
get
{
checkUpdate();
return isUpdate;
}
}
#endregion
在checkUpdate方法中,主要是通过对比本地的xml文件中的总版本信息和服务器端的总版本信息是否相同。不相同则是存在更新
#region 检查是否需要更新-比较本地的xml文件中的总版本信息和服务器端的总版本信息-王雷-2017年4月13日17:04:05
/// <summary>
/// 检查是否需要更新-比较本地的xml文件中的总版本信息和服务器端的总版本信息-王雷-2017年4月13日17:04:05
/// </summary>
public void checkUpdate()
{
try
{
//从本地的xml文件中提取出服务器的链接
string xmlLocal = Application.StartupPath + @"\UpdateList.xml";
XmlDocument xmlDoc = new XmlDocument();
xmlDoc.Load(xmlLocal);
XmlNode list = xmlDoc.SelectSingleNode("//Updater");
foreach (XmlNode node in list)
{
if (node.Name == "Url")
{
UrlServer = node.InnerText;
}
}
UrlServer = UrlServer + "/UpdateList.xml";
//获取服务端的版本号
string verServer = getVersion(UrlServer);
//获取本地的版本号
string verLocal = getVersion(xmlLocal);
//比较版本号
if (verServer != verLocal)
{
isUpdate = true; //需要更新
}
else
{
isUpdate = false;
}
}
catch (Exception ex)
{
throw new Exception("更新出现错误,请确认网络连接无误后重试!");
}
}
#endregion
在文件中存在根据xml文件的路径获取版本号Version节点下的值,这涉及到了读xml文件的知识。http://www.jb51.net/article/56289.htm博客可以介绍一下。对xml文件的增删改查。
#region 根据xml文件的路径获取版本号Version节点下的值-王雷-2017年4月13日17:05:05
/// <summary>
/// 根据xml文件的路径获取版本号Version节点下的值-王雷-2017年4月13日17:05:05
/// </summary>
/// <param name="URL">xml文件的路径</param>
/// <returns>string</returns>
public string getVersion(string URL)
{
WebClient wc = new WebClient();
Stream stream = wc.OpenRead(URL);
XmlDocument xmlDoc = new XmlDocument();
xmlDoc.Load(stream);
XmlNode list = xmlDoc.SelectSingleNode("//Update");
foreach (XmlNode node in list)
{
if (node.Name == "Soft" && node.Attributes["Name"].Value.ToLower() == SoftName.ToLower())
{
foreach (XmlNode xml in node)
{
if (xml.Name == "Verson")
newVerson = xml.InnerText;
else
download = xml.InnerText;
}
}
}
return newVerson;
}
#endregion
如果存在更新就会弹框显示:
3.4 显示手动更新页面
手动更新加载的页面流程:
1.从本地的配置文件读取出服务器的连接。
2.拼接出服务器上的配置文件的路径,获取服务器地址
3.与服务器连接,把服务器上的xml文件下载到建立的临时文件中。C:\Users\Ares\AppData\Local\Temp_ItemSoft_y_x_m_\
4.检查更新文件
#region 界面加载-检查出要更新的文件-王雷-2017年4月13日17:10:28
/// <summary>
/// 界面加载-检查出要更新的文件-王雷-2017年4月13日17:10:28
/// </summary>
/// <param name="sender"></param>
/// <param name="e"></param>
private void FrmUpdate_Load(object sender, System.EventArgs e)
{
panel2.Visible = false;
btnFinish.Visible = false;
//1.获取本地xml文件的路径
string localXmlFile = Application.StartupPath + "\\UpdateList.xml";
string serverXmlFile = string.Empty;
try
{
//从本地读取更新配置文件信息
updaterXmlFiles = new XmlFiles(localXmlFile);
}
catch
{
MessageBox.Show("配置文件出错!", "错误", MessageBoxButtons.OK, MessageBoxIcon.Error);
this.Close();
return;
}
//2.获取服务器地址
updateUrl = updaterXmlFiles.GetNodeValue("//Url");
AppUpdater appUpdater = new AppUpdater();
appUpdater.UpdaterUrl = updateUrl + "/UpdateList.xml";
//3.与服务器连接,下载更新配置文件
try
{
tempUpdatePath = Environment.GetEnvironmentVariable("Temp") + "\\" + "_" + updaterXmlFiles.FindNode("//Application").Attributes["applicationId"].Value + "_" + "y" + "_" + "x" + "_" + "m" + "_" + "\\";
//删除临时目录中的所有文件
DelectDir(tempUpdatePath);
//下载更新文件的临时目录
appUpdater.DownAutoUpdateFile(tempUpdatePath);
}
catch
{
MessageBox.Show("与服务器连接失败,操作超时!", "提示", MessageBoxButtons.OK, MessageBoxIcon.Information);
this.Close();
return;
}
//获取更新文件列表
Hashtable htUpdateFile = new Hashtable();
//拼接临时存放文件夹的路径
serverXmlFile = tempUpdatePath + "\\UpdateList.xml";
if (!File.Exists(serverXmlFile))
{
return;
}
//检查更新文件
availableUpdate = appUpdater.CheckForUpdate(serverXmlFile, localXmlFile, out htUpdateFile);
if (availableUpdate > 0)
{
for (int i = 0; i < htUpdateFile.Count; i++)
{
string[] fileArray = (string[])htUpdateFile[i];
lvUpdateList.Items.Add(new ListViewItem(fileArray));
}
}
}
#endregion
如果更新失败,就会在临时文件中存储已经下载的内容,对下次的尝试造成不便,所以对系统进行临时文件删除:
#region 删除因为错误而产生的临时文件-王雷-2017年4月21日10:24:34
/// <summary>
/// 删除因为错误而产生的临时文件-王雷-2017年4月21日10:24:34
/// </summary>
/// <param name="srcPath">临时文件目录</param>
public static void DelectDir(string srcPath)
{
try
{
DirectoryInfo dir = new DirectoryInfo(srcPath);
bool flag = dir.Exists;
if (flag)
{
FileSystemInfo[] fileinfo = dir.GetFileSystemInfos(); //返回目录中所有文件和子目录
foreach (FileSystemInfo i in fileinfo)
{
if (i is DirectoryInfo) //判断是否文件夹
{
DirectoryInfo subdir = new DirectoryInfo(i.FullName);
subdir.Delete(true); //删除子目录和文件
}
else
{
File.Delete(i.FullName); //删除指定文件
}
}
}
}
catch (Exception e)
{
throw;
}
}
#endregion
C:\Users\Ares\AppData\Local\Temp_ItemSoft_y_x_m_\
#region 返回下载更新文件的临时目录-王雷-2017年4月13日17:11:06
/// <summary>
/// 返回下载更新文件的临时目录-王雷-2017年4月13日17:11:06
/// </summary>
/// <returns></returns>
public void DownAutoUpdateFile(string downpath)
{
if (!System.IO.Directory.Exists(downpath))
System.IO.Directory.CreateDirectory(downpath);
string serverXmlFile = downpath + @"/UpdateList.xml";
try
{
WebRequest req = WebRequest.Create(this.UpdaterUrl);
WebResponse res = req.GetResponse();
if (res.ContentLength > 0)
{
try
{
WebClient wClient = new WebClient();
wClient.DownloadFile(this.UpdaterUrl, serverXmlFile);
}
catch
{
return;
}
}
}
catch
{
return;
}
//return tempPath;
}
#endregion
产生的临时文件目录,会把要更新的文件先下载到临时的文件中,起中转站的作用。
3.5 检查更新文件
通过对比从服务器上下载的xml文件和本地软件的xml软件来获得由多少条更新的记录
1.加载xml文件
2.把AutoUpdater/Files下的所有的子节点都存储在list中
3.遍历
4.取出newNodeList中节点名为Name,和Ver的值,和oldNodeList中的各个节点比较,如果两个都相同,则不用更新这条记录,否则需要更新。并把这条要更新的记录添加到updateFileList中。最后依次遍历updateFileList中的值,把信息显示到界面上。
#region 检查更新文件-王雷-2017年4月13日17:12:03
/// <summary>
/// 检查更新文件-王雷-2017年4月13日17:12:03
/// </summary>
/// <param name="serverXmlFile">服务器端xml文件的路径</param>
/// <param name="localXmlFile">本地xml文件的路径</param>
/// <param name="updateFileList">要更新文件的列表</param>
/// <returns></returns>
public int CheckForUpdate(string serverXmlFile, string localXmlFile, out Hashtable updateFileList)
{
updateFileList = new Hashtable();
if (!File.Exists(localXmlFile) || !File.Exists(serverXmlFile))
{
return -1;
}
//加载xml文件
XmlFiles serverXmlFiles = new XmlFiles(serverXmlFile);
XmlFiles localXmlFiles = new XmlFiles(localXmlFile);
//把AutoUpdater/Files下的所有的子节点都存储在list中
XmlNodeList newNodeList = serverXmlFiles.GetNodeList("AutoUpdater/Files");
XmlNodeList oldNodeList = localXmlFiles.GetNodeList("AutoUpdater/Files");
int k = 0;
for (int i = 0; i < newNodeList.Count; i++)
{
string[] fileList = new string[3];
string newFileName = newNodeList.Item(i).Attributes["Name"].Value.Trim();
string newVer = newNodeList.Item(i).Attributes["Ver"].Value.Trim();
ArrayList oldFileAl = new ArrayList();
for (int j = 0; j < oldNodeList.Count; j++)
{
string oldFileName = oldNodeList.Item(j).Attributes["Name"].Value.Trim();
string oldVer = oldNodeList.Item(j).Attributes["Ver"].Value.Trim();
oldFileAl.Add(oldFileName);
oldFileAl.Add(oldVer);
}
int pos = oldFileAl.IndexOf(newFileName);
if (pos == -1)
{
fileList[0] = newFileName;
fileList[1] = newVer;
updateFileList.Add(k, fileList);
k++;
}
else if (pos > -1 && newVer !=oldFileAl[pos + 1].ToString())
{
fileList[0] = newFileName;
fileList[1] = newVer;
updateFileList.Add(k, fileList);
k++;
}
}
return k;
}
#endregion
界面显示:
3.6 点击下一步,下载文件
下载效果:
在这里使用了BackgroundWorker组件,以及通过委托进行下载文件。
BackgroundWorker 组件用来执行诸如数据库事务、文件下载等耗时的异步操作。
#region 点击下一步-开始下载要更新的文件-存在的覆盖-王雷-2017年4月13日17:15:04
/// <summary>
/// 点击下一步-开始下载要更新的文件-存在的覆盖-王雷-2017年4月13日17:15:04
/// </summary>
/// <param name="sender"></param>
/// <param name="e"></param>
private void btnNext_Click(object sender, System.EventArgs e)
{
if (availableUpdate > 0)
{
using (BackgroundWorker bw = new BackgroundWorker())
{
bw.RunWorkerCompleted += new RunWorkerCompletedEventHandler(bw_RunWorkerCompleted);
bw.DoWork += new DoWorkEventHandler(DownUpdateFile);
bw.RunWorkerAsync();
}
}
//if (availableUpdate > 0)
//{
// Thread threadDown=new Thread(new ThreadStart(DownUpdateFile));
// threadDown.IsBackground = true;
// threadDown.Start();
//}
else
{
MessageBox.Show("没有可用的更新!", "自动更新", MessageBoxButtons.OK, MessageBoxIcon.Information);
return;
}
}
#endregion
#region 委托方法-线程完成结束操作-王雷-2017年4月13日17:16:42
/// <summary>
/// 委托方法-线程完成结束操作-王雷-2017年4月13日17:16:42
/// </summary>
/// <param name="sender"></param>
/// <param name="e"></param>
void bw_RunWorkerCompleted(object sender, RunWorkerCompletedEventArgs e)
{
//这时后台线程已经完成,并返回了主线程,所以可以直接使用UI控件了
this.Cursor = Cursors.Default;
}
#endregion
#region 下载文件-王雷-2017年4月13日17:15:52
/// <summary>
/// 下载文件-王雷-2017年4月13日17:15:52
/// </summary>
/// <param name="sender"></param>
/// <param name="e"></param>
private void DownUpdateFile(object sender, DoWorkEventArgs e)
{
//C#跨线程访问控件。
//http://www.cnblogs.com/TankXiao/p/3348292.html
//this.Cursor = Cursors.WaitCursor;
mainAppExe = updaterXmlFiles.GetNodeValue("//EntryPoint");
Process[] allProcess = Process.GetProcesses();
foreach (Process p in allProcess)
{
if (p.ProcessName.ToLower() + ".exe" == mainAppExe.ToLower())
{
for (int i = 0; i < p.Threads.Count; i++)
p.Threads[i].Dispose();
p.Kill();
isRun = true;
//break;
}
}
WebClient wcClient = new WebClient();
for (int i = 0; i < this.lvUpdateList.Items.Count; i++)
{
string UpdateFile = lvUpdateList.Items[i].Text.Trim();
string updateFileUrl = updateUrl + lvUpdateList.Items[i].Text.Trim();
long fileLength = 0;
try
{
WebRequest webReq = WebRequest.Create(updateFileUrl);
WebResponse webRes = webReq.GetResponse();
fileLength = webRes.ContentLength;
//fileLength = 100;
lbState.Text = "正在下载更新文件,请稍后...";
pbDownFile.Value = 0;
pbDownFile.Maximum = (int)fileLength;
Stream srm = webRes.GetResponseStream();
//StreamReader srmReader = new StreamReader(srm);
byte[] bufferbyte = new byte[fileLength];
int allByte = (int)bufferbyte.Length;
int startByte = 0;
while (fileLength > 0)
{
Application.DoEvents();
int downByte = srm.Read(bufferbyte, startByte, allByte);
if (downByte == 0) { break; };
startByte += downByte;
allByte -= downByte;
pbDownFile.Value += downByte;
float part = (float)startByte / 1024;
float total = (float)bufferbyte.Length / 1024;
int percent = Convert.ToInt32((part / total) * 100);
this.lvUpdateList.Items[i].SubItems[2].Text = percent.ToString() + "%";
}
UpdateFile = UpdateFile.Replace("/", "\\");
string tempPath = tempUpdatePath + UpdateFile;
CreateDirtory(tempPath);
FileStream fs = new FileStream(tempPath, FileMode.OpenOrCreate, FileAccess.Write);
fs.Write(bufferbyte, 0, bufferbyte.Length);
srm.Close();
//srmReader.Close();
fs.Close();
}
catch (WebException ex)
{
if (ex.Message.ToString()=="远程服务器返回错误: (404) 未找到。")
{
MessageBox.Show(UpdateFile+"更新文件下载失败!" , "错误", MessageBoxButtons.OK, MessageBoxIcon.Error);
return;
}
else
{
MessageBox.Show("更新文件下载失败!" + ex.Message.ToString(), "错误", MessageBoxButtons.OK, MessageBoxIcon.Error);
}
}
}
InvalidateControl();
this.Cursor = Cursors.Hand;
}
#endregion
#region 创建目录-王雷-2017年4月13日17:17:13
//创建目录
private void CreateDirtory(string path)
{
if (!File.Exists(path))
{
string[] dirArray = path.Split('\\');
string temp = string.Empty;
for (int i = 0; i < dirArray.Length - 1; i++)
{
temp += dirArray[i].Trim() + "\\";
if (!Directory.Exists(temp))
Directory.CreateDirectory(temp);
}
}
}
#endregion
3.6 下载完成,同步配置文件
完成效果:
最快的同步方法就是把服务器的文件复制到本地。
在这里要说明一下:如果我们要更新的是当前正在运行的进程,比如小编的是DESDecder.exe,那么我去更新它就会报“DESDecder.exe正在被另一个进程使用”的错误。所以我们要先把这个进程杀死,然后再去做更新的操纵。代码如下:
#region 点击完成复制更新文件到应用程序目录-王雷-2017年4月13日17:18:46
//点击完成复制更新文件到应用程序目录
private void btnFinish_Click(object sender, System.EventArgs e)
{
this.Close();
this.Dispose();
Process[] process = Process.GetProcesses();
foreach (Process prc in process)
{
if (prc.ProcessName == "DESDecder")
{
Thread t = new Thread(WriteY);
t.Start();
prc.Kill();
}
}
try
{
CopyFile(tempUpdatePath, Directory.GetCurrentDirectory());
System.IO.Directory.Delete(tempUpdatePath, true);
}
catch (Exception ex)
{
MessageBox.Show(ex.Message.ToString());
}
if (true == this.isRun) Process.Start(mainAppExe);
}
#endregion
复制文件:
#region 复制文件-王雷-2017年4月13日17:17:32
//复制文件;
public void CopyFile(string sourcePath, string objPath)
{
if (!Directory.Exists(objPath))
{
Directory.CreateDirectory(objPath);
}
string[] files = Directory.GetFiles(sourcePath);
for (int i = 0; i < files.Length; i++)
{
string[] childfile = files[i].Split('\\');
File.Copy(files[i], objPath + @"\" + childfile[childfile.Length - 1], true);
}
string[] dirs = Directory.GetDirectories(sourcePath);
for (int i = 0; i < dirs.Length; i++)
{
string[] childdir = dirs[i].Split('\\');
CopyFile(dirs[i], objPath + @"\" + childdir[childdir.Length - 1]);
}
}
#endregion
四、小结
通过这次的实践自己也是通过借鉴分析,对比来获得的,然后把代码一点一点的分析出来,写出来的。其中也借鉴了很多其他博主的博客。非常感谢他们,代码虽多,但是功能可以实现,总是软件更新这个方面的东西还是我们要深入学习的。加油!