转载 http://lovephoenix.iteye.com/blog/593078
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接口(Java代码)
1.package org.vv.hr.webservice.extra;
2.
3./**
4. * ISendFileWS WebService 接口
5. *
6. * @author 俞立全
7. * @date 2009-06-16
8. */
9.public interface ISendFileWS {
10.
11. /**
12. * 读取文件大小
13. *
14. * @param fileName
15. * @return
16. */
17. public long getFileSize(String fileName);
18.
19.
20. /**
21. * 客户端调用此方法,分块获取更新文件数据
22. *
23. * @param fileName 文件名
24. * @param offset 偏移值
25. * @param bufferSize 每次获取的大小
26. * @return 字节数组
27. */
28. public byte[] getUpdateFile(String fileName, int offset, int bufferSize);
29.
30. /**
31. * 以流形式上传文件至服务器
32. *
33. * @param fs
34. * @param fileName
35. * @return 服务器存储路径
36. */
37. public String uploadFile(byte[] fs, String fileName);
38.
39. /**
40. * 从服务器端以流形式下发文件
41. *
42. * @param path
43. * @return
44. */
45. public byte[] downFile(String path);
46.
47.}
实现代码很简单,各有各的写法,下面重点介绍思想。
客户端(C#代码 )
更新进度条采用多线程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 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),设置该值,对传输速度影响很大,环境越是恶劣,丢包明显,把值设小,可以加大稳定性。环境好,可以加大数值,加快传输速度。
C#代码:
private staticint 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 的性能优化方式很多,这里不进行讨论了。