B/S和C/S架构的融合——软件客户端通过WebService接口达到自动更新和上传数据,支持任意客户端语言环境。
测试用例:打开客户端自动下载更新文件,上传照片
服务器环境:Tomcat 6 、eclipse 3,测试 WebService 采用 spring 2.5 + xfire 1.2.6 ,目前可升级为 cxf 2.2.3
客户端环境:Microsoft VS2008 采用C#语言。
构建服务器
WS接口
package org.vv.hr.webservice.extra;
/**
* ISendFileWS WebService 接口
*
* @author 俞立全
* @date 2009-06-16
*/
public interface ISendFileWS {
/**
* 读取文件大小
*
* @param fileName
* @return
*/
public long getFileSize(String fileName);
/**
* 客户端调用此方法,分块获取更新文件数据
*
* @param fileName 文件名
* @param offset 偏移值
* @param bufferSize 每次获取的大小
* @return 字节数组
*/
public byte[] getUpdateFile(String fileName, int offset, int bufferSize);
/**
* 以流形式上传文件至服务器
*
* @param fs
* @param fileName
* @return 服务器存储路径
*/
public String uploadFile(byte[] fs, String fileName);
/**
* 从服务器端以流形式下发文件
*
* @param path
* @return
*/
public byte[] downFile(String path);
}
实现代码很简单,各有各的写法,下面重点介绍思想。
客户端: 更新进度条采用多线程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.IO; using System.Xml; using System.Threading; namespace HR_update { public partial class update : Form { /// <summary> /// 每次下载并写入磁盘的文件数据大小(字节) /// </summary> private static int BUFFER_SIZE = 128 * 1024; //把窗体改为单态模型 private static update updateForm; public static update getUpdateForm() { if (updateForm == null) { updateForm = new update(); } return updateForm; } //构造函数改为私有,外部程序不可以使用 new() 来创建新窗体,保证了窗体唯一性 private update() { //不检查线程间操作,容许子线呈随时更新ui,微软已经不推荐使用,这里用 invoke 回调代替 //CheckForIllegalCrossThreadCalls = false; InitializeComponent(); } //******** 定义代理方法,解决多线程环境中跨线程改写 ui 控件属性。start ******** //定义设置一个文本的委托方法(字符串) 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) { progressBar1.Value += count; } //设置单文件进度条步进进度 private void addProgressBar2_value(int count) { progressBar2.Value += count; } //设置文本框的值 private void UpdateText(string log) { textBox1.Text += log; } //******** 定义代理方法,解决多线程环境中跨线程改写 ui 控件属性。 end ******** /// <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); } //通过 webservice 从服务器端获取更新脚本文件 update.xml getUpdateXMLFile(); //判断强制更新开关 if (isForceUpdate()) { //通过 webservice 从服务器端下载更新程序文件 downloadFiles(); } else { //比较版本号 if (verifyVersion()) { //通过 webservice 从服务器端下载更新程序文件 downloadFiles(); } } //启动客户端主程序,退出更新程序 appExit(); } /// <summary> /// 下载 update.xml /// </summary> private void getUpdateXMLFile() { //执行委托方法,更新文本控件内容 textBox1.Invoke(new setText(this.UpdateText), new object[] { "正在从服务器下载 更新脚本文件 update.xml \r\n" }); //创建一个文件传送的 webservice 接口实例 SendFileWS.ISendFileWS sendFileWS = new HR_update.SendFileWS.ISendFileWS(); //通过 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) { 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 (Exception e) { //发生异常,则更新程序,覆盖 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" }); progressBar1.Invoke(new setProcess(this.setProgressBar1_Maximum), new object[] { fileCount }); //结束 HRClient.exe 进程? System.Diagnostics.Process[] processes = System.Diagnostics.Process.GetProcesses(); foreach (System.Diagnostics.Process process in processes) { if (process.ProcessName == "HRClient.exe") { process.Close(); break; } } //循环文件列表 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 接口 SendFileWS.ISendFileWS sendFileWS = new HR_update.SendFileWS.ISendFileWS(); //获取文件长度(字节) 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) { //将下载的更新文件写入程序目录的 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 }); } //替换文件 try { if (fileName != "HR_update.XmlSerializers.dll" || fileName != "HR_update.exe.config" || fileName != "HR_update.pdb" || fileName != "HR_update.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 }); } //最后复制更新信息文件 File.Copy("update/update.xml" , "update.xml", true); } /// <summary> /// 启动客户端主程序,退出更新程序 /// </summary> private void appExit() { //获取主程序执行文件名 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(); } } }
客户端启动更新截图如下:采用双进度条。
下面讲讲客户端更新的流程
客户端首先从服务器下载 xml 更新配置文件,xml更新配置文件的信息如下:
<?xml version="1.0" encoding="UTF-8"?> <update> <forceUpdate>false</forceUpdate> <version>20080104</version> <subversion>3</subversion> <filelist count="24"> <file name="CommonLibrary.dll">true</file> <file name="CommonLibrary.pdb">true</file> <file name="HR_update.exe">true</file> <file name="HR_update.exe.config">true</file> <file name="HR_update.pdb">true</file> <file name="HR_update.XmlSerializers.dll">true</file> <file name="HRClient.exe">true</file> <file name="HRClient.exe.config">true</file> <file name="HRClient.pdb">true</file> </filelist> <executeFile>HRClient.exe</executeFile> </update>
forceUpdate 节点默认为false ,如果设为true,表示每次更新不比较版本号,下载所有文件并覆盖。
version 节点为主版本号
subversion 节点为子版本号
filelist 包含了更新的文件列表
file name 为文件名 属性true表示需要更新。
executeFile 为更新程序执行完成后自动调用的主应用程序文件名,这样更新程序和主程序完全解耦,可以应用到其它系统中。
在客户端的源码中设置了每次向服务器请求的数据块大小。这也是在服务器端代码中我之前注释掉的地方,这行代码在实际应用中,应该放在客户端配置文件中,这个参数很有意义,经过测试,在不同的网络环境中(1000M、100M、10M),设置该值,对传输速度影响很大,环境越是恶劣,丢包明显,把值设小,可以加大稳定性。环境好,可以加大数值,加快传输速度。
private static int BUFFER_SIZE = 128 * 1024;
客户端上传照片的接口代码如下,也是调用了服务器的 ws 接口
WSFactory.getSendFileWS().uploadFile(commonBusiness.getBinaryFile(FileName), SafeFileName);
可以看到,J2EE 和 .NET 通过ws 还是可以很好的工作在一起的,J2EE 通过 ws 把 接口开放给 客户端,把原先客户端的公共业务逻辑放到了服务器来执行,客户端只是提交用户数据并接收服务器反馈的数据给用户,这使得客户端变得很轻,.net 提供了丰富的ui 界面组建,又大大弥补了传统J2EE B/S 架构用户界面的体验贫乏的特点。
通过WS ,J2EE 还可以和任何支持 ws 接口的语言结合,如C++、vb 、甚至 windows7 自带的 PowerShell 脚本。
这种方式灵活性很高,面对一个需求,可以有多种解决方案。可以把部分特殊业务放在客户端完成,服务器只提供权限,事务控制和持久化,也可以把业务放在服务器,客户端注意力放在用户体验上,如果单个业务繁重,还可以把服务器业务进行横向或纵向切分,多服务器节点之间通过ws传递数据。
为了加快传递速度 ws 的性能优化方式很多,这里不进行讨论了。