B/S和C/S架构的融合,软件客户端通过WebService接口达到自动更新和上传数据

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 的性能优化方式很多,这里不进行讨论了。

评论 1
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值