C/S架构系统的自动更新功能设计与实现(四)

C/S架构Winform客户端自动更新技术实现 htmltable {th, td {th {pre {简介:本项目重点在于开发C/S架构下Winform客户端的自动更新机制,以.NET框架为基础,简化更新流程,减少用户手动操作。通过服务端FTP服务器存储更新包,客户端在运行时检查版本并提示更新。用户同意后,客户端通过网络协议下载、解压缩新版本包,并替换旧文件,最终刷新应用以应用更新。过程中还需要处理版本控制、文件完整性校验及数据库迁移等技术要点。 阅读详情

客户端配置管理

基于WPF开发客户端。配置文件写在config文件中。

<?xml version="1.0" encoding="utf-8" ?>
<configuration>
  <appSettings>
    <!--升级服务器的IP地址-->
    <add key="ServerIP" value="127.0.0.1" />
    <!--升级服务器的端口号-->
    <add key="ServerPort" value="20000" />
    <!--升级后要调起的应用程序名(与本升级组件同级目录)-->
    <add key="Callback" value="XXXX.XXXX.exe" />
  </appSettings>
    <startup> 
        <supportedRuntime version="v4.0" sku=".NETFramework,Version=v4.7.2" />
    </startup>
</configuration>

配置管理类

/// <summary>
/// 配置项的关键字(名称)
/// </summary>
public class ConfigKey
{
	/// <summary>
	/// 服务器IP
	/// </summary>
	public static string m_sKeyServerIP = "ServerIP";
	/// <summary>
	/// 服务器端口号
	/// </summary>
	public static string m_sKeyServerPort = "ServerPort";
	/// <summary>
	/// 更新后启动的应用程序名
	/// </summary>
	public static string m_sKeyCallback = "Callback";
}
/// <summary>
/// 
/// </summary>
public class ConfigManager
{
	Configuration config = null;
	/// <summary>
	/// 
	/// </summary>
	public ConfigManager()
	{
		config = ConfigurationManager.OpenExeConfiguration(ConfigurationUserLevel.None);
	}

	/// <summary>
	/// 
	/// </summary>
	/// <returns></returns>
	public Dictionary<string, string> GetAppSettings()
	{
		var dict = new Dictionary<string, string>();
		config.AppSettings.Settings.AllKeys.ToList().ForEach(p => dict.Add(p, config.AppSettings.Settings[p].Value));
		return dict;
	}

	/// <summary>
	/// //添加键值
	/// </summary>
	/// <param name="key"></param>
	/// <param name="value"></param>
	public void AddAppSetting(string key, object value)
	{
		config.AppSettings.Settings.Add(key, value?.ToString());
		config.Save();
	}

	/// <summary>
	/// //修改键值
	/// </summary>
	/// <param name="key"></param>
	/// <param name="value"></param>
	public void SaveAppSetting(string key, object value)
	{
		config.AppSettings.Settings.Remove(key);
		config.AppSettings.Settings.Add(key, value?.ToString());
		config.Save();
	}

	/// <summary>
	/// //获得键值
	/// </summary>
	/// <param name="key"></param>
	/// <returns></returns>
	public string GetAppSetting(string key)
	{
		return config.AppSettings.Settings[key]?.Value;
	}

	/// <summary>
	/// //移除键值
	/// </summary>
	/// <param name="key"></param>
	public void DeleteAppSetting(string key)
	{
		config.AppSettings.Settings.Remove(key);
		config.Save();
	}
}

功能入口管理

添加一个MainApp,并设置其为启动对象:
设置启动对象
添加参数传递以支持应用程序内部调用该更新程序:

/// <summary>
/// 实际的启动对象
/// </summary>
public class MainApp
{
	/// <summary>
	/// 主函数入口
	/// </summary>
	/// <param name="args">附加参数(要自动关闭的进程名列表)</param>
	[STAThread]
	public static void Main(string[] args)
	{
		try
		{
			#region 关闭传递过来的进程
			args.ToList().ForEach(ar =>
			{
				Process.GetProcessesByName(ar.ToLower().Replace(".exe", "")).ToList().ForEach(p => p.Kill());
			});
			#endregion

			#region 关闭要回调启动的进程
			var sCallback = new ConfigManager().GetAppSetting(ConfigKey.m_sKeyCallback);
			if (!string.IsNullOrEmpty(sCallback))
			{
				var sCallbackName = System.IO.Path.Combine(AppDomain.CurrentDomain.BaseDirectory, sCallback);
				Process.GetProcessesByName(sCallback.ToLower().Replace(".exe", "")).ToList().ForEach(p =>
				{
					if (0 == string.Compare(p.MainModule.FileName, sCallbackName, StringComparison.OrdinalIgnoreCase))
					{
						p.Kill(); //仅关闭当前目录下的进程(防止错误关闭其他进程)
				}
				});
			}
			#endregion
		}
		catch (Exception)
		{

		}

		#region 启动应用程序
		var app = new App();
		app.InitializeComponent();
		app.Run();
		#endregion
	}
}

界面设计

添加一个简单的WPF界面。
更新功能界面设计

<Window x:Class="XXXX.AutoUpgrade.MainWindow"
        xmlns="http://schemas.microsoft.com/winfx/2006/xaml/presentation"
        xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml"
        xmlns:d="http://schemas.microsoft.com/expression/blend/2008"
        xmlns:mc="http://schemas.openxmlformats.org/markup-compatibility/2006"
        mc:Ignorable="d"
        Title="  系统升级" Height="396" Width="640" WindowStartupLocation="CenterScreen" ResizeMode="NoResize" ShowInTaskbar="False" Background="White" WindowStyle="None" Closed="Window_Closed">
    <Grid Height="396" VerticalAlignment="Top" Background="{DynamicResource {x:Static SystemColors.WindowBrushKey}}">
        <Image x:Name="image" HorizontalAlignment="Left" VerticalAlignment="Top" Width="640" Source="Resources/autoupgrade.jpg"/>
        <Label x:Name="labelLocalVersion" Content="当前版本: 1.0.9.0 更新时间: 0000/0/00 00:00:00" Margin="10,10,331,0" VerticalAlignment="Top" RenderTransformOrigin="0.512,-1.28" Height="26" Background="Transparent" FontWeight="Bold">
            <Label.Foreground>
                <SolidColorBrush Color="{DynamicResource {x:Static SystemColors.ControlColorKey}}"/>
            </Label.Foreground>
        </Label>
        <Label x:Name="labelNewVersion" Content="最新版本: 0.0.0.0 发布时间: 0000/0/00 00:00:00" HorizontalAlignment="Right" Margin="0,10,10,0" VerticalAlignment="Top" Width="290" Height="26" Background="Transparent" FontWeight="Bold">
            <Label.Foreground>
                <SolidColorBrush Color="{DynamicResource {x:Static SystemColors.ControlColorKey}}"/>
            </Label.Foreground>
        </Label>
        <ProgressBar x:Name="progressInfo" Margin="10,360,10,0" Background="#FFE6E6E6" BorderThickness="1" Height="4" VerticalAlignment="Top"/>
        <Label x:Name="labelProgressTip" Content="等待中..." Margin="0,330,126,0" RenderTransformOrigin="0.512,-1.28" HorizontalContentAlignment="Left" Height="26" VerticalAlignment="Top" HorizontalAlignment="Right" Width="435" FontWeight="Bold">
            <Label.Foreground>
                <SolidColorBrush Color="{DynamicResource {x:Static SystemColors.ActiveCaptionTextColorKey}}"/>
            </Label.Foreground>
        </Label>
        <Label x:Name="labelProgressFile" Content="等待中..." Margin="0,368,10,3" RenderTransformOrigin="0.512,-1.28" HorizontalContentAlignment="Left" HorizontalAlignment="Right" Width="551" FontWeight="Bold">
            <Label.Foreground>
                <SolidColorBrush Color="{DynamicResource {x:Static SystemColors.ActiveCaptionTextColorKey}}"/>
            </Label.Foreground>
        </Label>
        <Label x:Name="label" Content="更新进度: " HorizontalAlignment="Left" Margin="10,330,0,0" VerticalAlignment="Top" Height="26" Width="64" FontWeight="Bold">
            <Label.Foreground>
                <SolidColorBrush Color="{DynamicResource {x:Static SystemColors.ActiveCaptionTextColorKey}}"/>
            </Label.Foreground>
        </Label>
        <Label x:Name="labelOperation" Content="更新文件: " HorizontalAlignment="Left" Margin="10,368,0,0" VerticalAlignment="Top" Height="26" Width="64" FontWeight="Bold">
            <Label.Foreground>
                <SolidColorBrush Color="{DynamicResource {x:Static SystemColors.ActiveCaptionTextColorKey}}"/>
            </Label.Foreground>
        </Label>
        <Label x:Name="labelProgressValue" Content="0%" HorizontalAlignment="Left" Margin="566,330,0,0" VerticalAlignment="Top" Height="26" Width="64" FontWeight="Bold" RenderTransformOrigin="8.655,0.678" HorizontalContentAlignment="Right">
            <Label.Foreground>
                <SolidColorBrush Color="{DynamicResource {x:Static SystemColors.ActiveCaptionTextColorKey}}"/>
            </Label.Foreground>
        </Label>
    </Grid>
</Window>

更新功能实现

成员变量

        private ConfigManager m_config = new ConfigManager();

        private string m_sValueServerIP = "10.0.1.26";
        private int m_nValueServerPort = 20000;
        private string m_sValueCallback = "";

        private string m_sMainFolder = AppDomain.CurrentDomain.BaseDirectory; //主程序目录

        private string m_sCacheFolder = Path.Combine(AppDomain.CurrentDomain.BaseDirectory, "UpgradeCache"); //升级文件缓存目录

主窗口

public MainWindow()
{
	InitializeComponent();
	try
	{
		#region 获取配置项中的服务器端IP和端口Port
		var sValueServerIP = m_config.GetAppSetting(ConfigKey.m_sKeyServerIP);
		if (!string.IsNullOrEmpty(sValueServerIP))
		{
			if (IPAddress.TryParse(sValueServerIP, out IPAddress address))
			{
				m_sValueServerIP = sValueServerIP;
			}
			else
			{
				ShowError("config中配置的ServerIP项非法!");
			}
		}
		m_config.SaveAppSetting(ConfigKey.m_sKeyServerIP, m_sValueServerIP);

		var sValueServerPort = m_config.GetAppSetting(ConfigKey.m_sKeyServerPort);
		if (!string.IsNullOrEmpty(sValueServerPort))
		{
			if (int.TryParse(sValueServerPort, out int nPort))
			{
				m_nValueServerPort = nPort;
			}
			else
			{
				ShowError("config中配置的ServerPort项非法!");
			}
		}
		m_config.SaveAppSetting(ConfigKey.m_sKeyServerPort, m_nValueServerPort);
		#endregion

		#region 获取配置项中的回调程序名(位于本自动更新程序同级目录中,如:XXXX.XXXX.exe)
		var sValueCallback = m_config.GetAppSetting(ConfigKey.m_sKeyCallback);
		if (!string.IsNullOrEmpty(sValueCallback))
		{
			m_sValueCallback = sValueCallback;
		}
		m_config.SaveAppSetting(ConfigKey.m_sKeyCallback, m_sValueCallback);
		#endregion

		#region 获取服务器端最新版本摘要
		var sAbstract = RequestWeb("api/Upgrade/Abstract");
		if (!ValidJson.IsJson(sAbstract))
		{
			throw new Exception("获取服务器端最新版本摘要信息失败,请检查升级文件配置及网络状态!");
		}
		var responseInfoAbstract = JsonConvert.DeserializeObject<ResponseInfo>(sAbstract);
		if (ErrorCode.Ok != responseInfoAbstract.Code)
		{
			ShowWarning("未能获取升级信息,请检查升级文件配置及网络状态!");
			Close();
		}
		#endregion

		#region 获取本地版本,与最新版本进行比较
		var localUpgradeInfo = new UpgradeInfo();
		localUpgradeInfo.Load();
		var upgradeInfoAbstract = JsonConvert.DeserializeObject<UpgradeInfoAbstract>(responseInfoAbstract.Data.ToString());
		if (upgradeInfoAbstract.Equals(new UpgradeInfoAbstract(localUpgradeInfo)))
		{
			//ShowInfo("当前版本已是最新版,不需要升级!");
			Close();
		}
		#endregion

		#region 获取升级文件列表
		var sFiles = RequestWeb("api/Upgrade/Files");
		if (!ValidJson.IsJson(sFiles))
		{
			throw new Exception("获取服务器端升级文件信息失败,请检查升级文件配置及网络状态!");
		}
		var responseInfoFilesInfo = JsonConvert.DeserializeObject<ResponseInfo>(sFiles);
		if (ErrorCode.Ok != responseInfoFilesInfo.Code)
		{
			ShowWarning("未能获取升级文件列表,请检查升级文件配置及网络状态!");
			Close();
		}
		var filesInfo = JsonConvert.DeserializeObject<UpgradeFilesInfo>(responseInfoFilesInfo.Data.ToString());
		if (null == filesInfo || null == filesInfo.FileUnits || 0 >= filesInfo.FileUnits.Count)
		{
			ShowWarning("升级文件列表为空,不需要升级!");
			Close();
		}
		#endregion

		#region 升级前先清空缓存,重建缓存目录
		if (Directory.Exists(m_sCacheFolder))
			Directory.Delete(m_sCacheFolder, true);
		Directory.CreateDirectory(m_sCacheFolder);
		#endregion

		#region 执行版本升级
		Upgrade(localUpgradeInfo, upgradeInfoAbstract, filesInfo);
		#endregion
	}
	catch (Exception ex)
	{
		ShowError(ex.Message);
		Close();
	}
}

升级过程

/// <summary>
/// 升级过程
/// </summary>
/// <param name="localUpgradeInfo"></param>
/// <param name="upgradeInfoAbstract"></param>
/// <param name="filesInfo"></param>
private void Upgrade(UpgradeInfo localUpgradeInfo, UpgradeInfoAbstract upgradeInfoAbstract, UpgradeFilesInfo filesInfo)
{
	Task.Factory.StartNew(() =>
	{
		#region 显示升级信息
		var sLocalVersion = $"当前版本: {(string.IsNullOrEmpty(localUpgradeInfo.MainVersion) ? "N/A" : localUpgradeInfo.MainVersion)}  升级时间: {localUpgradeInfo.LastUpgradeTime.ToString()}";
		new Action(() =>
		{
			Dispatcher.BeginInvoke(new Action(() =>
			{
				labelLocalVersion.Content = sLocalVersion;
			}), System.Windows.Threading.DispatcherPriority.SystemIdle, null);
		}).BeginInvoke(null, null);

		var sNewVersion = $"最新版本: {upgradeInfoAbstract.MainVersion}  发布时间: {upgradeInfoAbstract.LastUpgradeTime.ToString()}";
		new Action(() =>
		{
			Dispatcher.BeginInvoke(new Action(() =>
			{
				labelNewVersion.Content = sNewVersion;
			}), System.Windows.Threading.DispatcherPriority.SystemIdle, null);
		}).BeginInvoke(null, null);
		#endregion

		#region 下载升级文件到缓存目录
		var nTotalCount = upgradeInfoAbstract.TotalCount;
		var dTotalSize = Math.Round(upgradeInfoAbstract.TotalSize * 0.001, 2); //单位从KB转为MB(粗略计算,除以1000。保留两位小数)
		var sTotalSize = string.Format("{0:N2}", dTotalSize);
		new Action(() =>
		{
			Dispatcher.BeginInvoke(new Action(() =>
				{
					progressInfo.Minimum = 0;
					progressInfo.Maximum = nTotalCount;
				}), System.Windows.Threading.DispatcherPriority.SystemIdle, null);
		}).BeginInvoke(null, null);

		double dCurrentSize = 0;
		var lstFailedFile = new List<string>(); //升级失败的文件列表
		for (int i = 0; i < nTotalCount; i++)
		{
			var bSkip = false;
			var fileInfo = filesInfo.FileUnits[i];
			dCurrentSize += fileInfo.FileSize;
			var sLocalPath = Path.Combine(m_sMainFolder, fileInfo.RelativePath);
			if (File.Exists(sLocalPath))
			{
				bSkip = new FileUnit(m_sMainFolder, sLocalPath).Equals(fileInfo);
			}
			var sCurrentFile = "";
			if (fileInfo.FileSize < 100) //小于100KB的,按KB显示,否则按MB显示
			{
				sCurrentFile = $"{fileInfo.RelativePath} ({fileInfo.FileSize} KB)";
			}
			else
			{
				sCurrentFile = $"{fileInfo.RelativePath} ({string.Format("{0:N2}", Math.Round(fileInfo.FileSize * 0.001, 2))} MB)";
			}
			if (!bSkip)
			{
				if (!RequestWebFile("api/Upgrade/Stream", fileInfo.RelativePath) && lstFailedFile.Count < 11)
				{
					lstFailedFile.Add(fileInfo.RelativePath);
				}
			}
			new Action(() =>
			{
				Dispatcher.BeginInvoke(new Action(() =>
				{
					var sProgressInfo = $"{(i).ToString("N0")}/{nTotalCount.ToString("N0")}个, {string.Format("{0:N2}", dCurrentSize * 0.001)}/{sTotalSize} MB";
					labelProgressFile.Content = sCurrentFile;
					labelProgressTip.Content = sProgressInfo;
					labelProgressValue.Content = $"{ Math.Round((double)((100 * i) / nTotalCount), 2)} %";
					progressInfo.Value = i + 1;
				}), System.Windows.Threading.DispatcherPriority.SystemIdle, null);
			}).BeginInvoke(null, null);
		}
		#endregion

		#region 判断是否有下载失败的文件
		var sFaileFileList = "";
		var nFailedCount = lstFailedFile.Count > 10 ? 10 : lstFailedFile.Count;
		if (0 < nFailedCount)
		{
			for (int i = 0; i < nFailedCount; i++)
			{
				if (0 == i)
				{
					sFaileFileList += $"[{i + 1}] {lstFailedFile[i]}";
				}
				else
				{
					sFaileFileList += $"{Environment.NewLine}[{i + 1}] {lstFailedFile[i]}";
				}
			}
			if (lstFailedFile.Count > 10)
			{
				sFaileFileList += $"{Environment.NewLine}...";
			}
		}
		#endregion

		#region 文件全部下载后,判断是否更新成功
		if (!string.IsNullOrEmpty(sFaileFileList))
		{
			Dispatcher.Invoke(new Action(delegate
			{
				labelProgressValue.Content = "100%";
				ShowError($"升级失败!以下文件未能正确更新:{Environment.NewLine}{sFaileFileList}");
				Directory.Delete(m_sCacheFolder, true); //删除缓存目录
				Close();
			}));
		}
		else
		{
			new Action(() =>
			{
				Dispatcher.BeginInvoke(new Action(() =>
				{
					labelOperation.Content = "拷贝文件: ";
					progressInfo.Value = 0;
				}), System.Windows.Threading.DispatcherPriority.SystemIdle, null);
			}).BeginInvoke(null, null);
			for (int i = 0; i < nTotalCount; i++) //拷贝文件
			{
				var fileInfo = filesInfo.FileUnits[i];
				var sNewPath = Path.Combine(m_sCacheFolder, fileInfo.RelativePath);
				var sOldPath = Path.Combine(m_sMainFolder, fileInfo.RelativePath);
				new Action(() =>
				{
					Dispatcher.BeginInvoke(new Action(() =>
					{
						var sProgressInfo = $"{i.ToString("N0")}/{nTotalCount.ToString("N0")}";
						labelProgressFile.Content = fileInfo.RelativePath;
						labelProgressTip.Content = sProgressInfo;
						labelProgressValue.Content = $"{ Math.Round((double)((100 * i) / nTotalCount), 2)} %";
						progressInfo.Value = i + 1;
					}), System.Windows.Threading.DispatcherPriority.SystemIdle, null);
				}).BeginInvoke(null, null);

				if (File.Exists(sNewPath))
				{
					if (File.Exists(sOldPath))
					{
						File.Delete(sOldPath);
					}
					var sParentDir = new FileInfo(sOldPath).Directory.FullName;
					if (!Directory.Exists(sParentDir))
						Directory.CreateDirectory(sParentDir);
					File.Move(sNewPath, sOldPath);
				}
			}
			Directory.Delete(m_sCacheFolder, true); //删除缓存目录

			//获取完整版升级信息并保存到本地配置文件
			var sInfo = RequestWeb("api/Upgrade/Info");
			if (ValidJson.IsJson(sInfo))
			{
				var responseInfo = JsonConvert.DeserializeObject<ResponseInfo>(sInfo);
				if (ErrorCode.Ok == responseInfo.Code)
				{
					JsonConvert.DeserializeObject<UpgradeInfo>(responseInfo.Data?.ToString()).Save();
				}
			}

			Dispatcher.Invoke(new Action(delegate
			{
				labelProgressValue.Content = "100%";
				//ShowInfo("升级成功!");
				Close();
			}));
		}
		#endregion
	});
}

其他方法

/// <summary>
/// 发起网络请求
/// </summary>
/// <param name="sRequestAddress">请求地址(不包括IP、端口)</param>
/// <param name="sParam">附加参数</param>
/// <returns>返回结果</returns>
private string RequestWeb(string sRequestAddress, string sParam = null)
{
	string sResponse;
	try
	{
		var sServerIP = m_sValueServerIP;
		if (0 == string.Compare(sServerIP, "127.0.0.1", StringComparison.OrdinalIgnoreCase))
			sServerIP = "localhost";
		var nServerPort = m_nValueServerPort;
		var sUrl = $"http://{sServerIP}:{nServerPort}/{sRequestAddress}";
		if (!string.IsNullOrEmpty(sParam))
		{
			sUrl = $"{sUrl}?param={sParam}";
		}
		var request = (HttpWebRequest)WebRequest.Create(sUrl);
		request.Method = "GET";
		request.ContentType = "application/json";
		var responseStream = request.GetResponse();
		var response = (HttpWebResponse)request.GetResponse();
		var streamReader = new StreamReader(response.GetResponseStream(), Encoding.UTF8);
		sResponse = streamReader.ReadToEnd();
		streamReader.Close();
		if (response != null)
		{
			response.Close();
		}
		if (request != null)
		{
			request.Abort();
		}
	}
	catch (Exception ex)
	{
		sResponse = ex.Message;
	}
	return sResponse;
}

/// <summary>
/// 发起网络请求以下载文件
/// </summary>
/// <param name="sRequestAddress">请求地址(不包括IP、端口)</param>
/// <param name="sFileName">文件名(相对路径)</param>
/// <returns>是否成功</returns>
private bool RequestWebFile(string sRequestAddress, string sFileName)
{
	var bResult = false;
	try
	{
		var sServerIP = m_sValueServerIP;
		if (0 == string.Compare(sServerIP, "127.0.0.1", StringComparison.OrdinalIgnoreCase))
			sServerIP = "localhost";
		var nServerPort = m_nValueServerPort;
		var sUrl = $"http://{sServerIP}:{nServerPort}/{sRequestAddress}";
		if (!string.IsNullOrEmpty(sFileName))
		{
			sUrl = $"{sUrl}?param={sFileName}";
		}
		var request = (HttpWebRequest)WebRequest.Create(sUrl);
		request.Method = "GET";
		var response = (HttpWebResponse)request.GetResponse();
		if (HttpStatusCode.OK == response.StatusCode)
		{
			var sFile = Path.Combine(m_sCacheFolder, sFileName);
			var sParentDir = new FileInfo(sFile).Directory.FullName;
			if (!Directory.Exists(sParentDir))
				Directory.CreateDirectory(sParentDir);
			if (File.Exists(sFile))
				File.Delete(sFile);
			using (var fileStream = File.Create(sFile))
			{
				using (var stream = response.GetResponseStream())
				{
					stream.CopyTo(fileStream);
					bResult = true;
				}
			}
		}
		if (response != null)
		{
			response.Close();
		}
		if (request != null)
		{
			request.Abort();
		}
	}
	catch
	{
		bResult = false;
	}
	return bResult;
}

/// <summary>
/// 窗口关闭后的回调处理
/// </summary>
/// <param name="sender"></param>
/// <param name="e"></param>
private void Window_Closed(object sender, EventArgs e)
{
	if (string.IsNullOrEmpty(m_sValueCallback))
	{
		return;
	}
	var sCallback = Path.Combine(AppDomain.CurrentDomain.BaseDirectory, m_sValueCallback);
	if (!File.Exists(sCallback))
	{
		ShowError($"启动失败,原因:文件不存在!路径:{Environment.NewLine}{sCallback}");
		return;
	}
	System.Diagnostics.Process.Start(sCallback);
}

集成方式

0代码集成!!!
将升级功能组件修改为实际要启动的应用程序名,并将回调应用程序设置为真正的启动对象即可。
比如,我原来的应用程序叫"abc.exe",则将此升级组件改名为“我的ABC.exe”,其回调的应用程序指向原来的"abc.exe"即可。

升级功能界面

功能界面

构建完整的C/S架构软件在线升级系统 本文还有配套的精品资源,点击获取 简介:该项目是一个为客户端软件提供自动更新功能的C/S架构应用程序升级解决方案。它包含一套用于实现在线更新机制的完整代码框架,并涵盖了从源码分析到部署发布的全部关键技术和组件。开发者可以学习如何设计和实现软件版本检查、文件下载、错误处理等环节,以及如何提供良好的用户体验。 1. C/S架构概述 C/S架构,即客户端/服务器架... 阅读详情

相关推荐

构建C/S程序的自动版本控制与更新系统

htmltable {th, td {th {pre {简介:C/S架构程序依赖客户端和服务器端的协同工作,保持版本同步对于确保用户获得最新的功能和安全更新至关重要。本文介绍了一款自动更新工具,其通过HTTP协议实现客户端的自动升级,包括检测更新、下载安装包、验证更新文件完整性、安装更新以及启动新版本等步骤。开发者需要考虑安全性措施如加密通信和防止篡改,以确保更新过程的安全。这种工具对于简化软件维护和提升用户体验至关重要,并是企业信息化建设的关键。

weixin_30205153的博客 670

C/S架构系统的自动更新功能设计与实现(一)

文章目录需求背景功能设计服务器端版本库目录服务器端版本更新服务接口客户端与服务器通信机制客户端更新机制 需求背景 项目上要开发一个C/S架构的系统,在客户单位的内网上使用。客户要求,系统上线后,有版本更新时,要能够自动检测新版本并获取更新,而不需要每次使用U盘等介质拷贝安装新版本。基于此需求,我们需要设计一套可以在线更新的C/S架构,在系统启动时,检测服务器有无更新版本,如果有的话,就自动下载新版...

a_dev的专栏 3076

B/S与C/S架构深度解析与对比实战

使用OpenAPI(Swagger)定义API契约:paths:/users:get:summary: 获取用户列表in: queryresponses:'200':description: 成功返回用户列表content:schema:支持自动生成文档与客户端SDK,降低沟通成本。

weixin_26854475的博客 627

WPF中的数据库与服务端的交互(C/S)架构版

作者:郑凯丰 开发工具:VS、SQL Server、WPF 编写日期:2019年6月3日 代码: WPF中无论是增删查改哪一种首先我们第一步都是在数据库里面先写出相应的代码; 例如: 先要创建一个数据层(DALMeth) 3.然后我们在数据层里面写下一步,利用服务端(Server)去连接数据库; 3. 然后在下面写数据库与服务端连接完成后所需要交互的代码; (这样便可以实现数据库...

weixin_44540479的博客 1510

B/S架构系统通用客户端实现与应用

B/S架构(Browser/Server,浏览器/服务器模式),是一种将数据处理和业务逻辑集中在服务器端,而用户界面则通过Web浏览器呈现给用户的软件架构模式。在当今信息化时代,B/S架构凭借其众多优点,如易于部署、跨平台性强、便于维护与升级等,在企业信息系统建设中占据着主导地位。JavaScript是构建动态网页和开发富交互式应用程序的核心技术之一。它是一种轻量级的编程语言,可以直接在浏览器中运行,支持事件驱动、对象导向和函数式编程等多种编程范式。

weixin_35949153的博客 1392

C/S程序版本控制(自动更新)工具

C/S客户端版本自动升级工具,使用http协议,详细请看附件中的说明文档。

C# C/S架构软件的自动更新功能(http方式) 代码1

using System;using System.Collections.Generic;using System.ComponentModel;using System.Data;using System.Drawing;using System.Text;using System.Windows.Forms; using System.Collections;using System.Ne...

yueqing的专栏 494

C/s架构客户端自动更新软件

C/S应用程序,客户端自动上传下载软件,嵌套客户端应用程序,用户直接运行更新软件即可自动下载新版本。解决了传统的C/S客户端更新麻烦的问题。

WPF中的修改(C/S)架构版

作者:郑凯丰 开发工具:VS、SQL Server、WPF 编写日期:2019年6月3日 代码: 在数据库写好修改的代码; 然后在数据库与服务端连接完成的前提下创建一个类里面写接收数据库的数据 在下面写修改的代码; 先在客户端的用户控件那里写好修改按钮的触发事件; (在修改页面那里写 确认按钮、重置按钮、取消按钮的事件) 实例化接收数据的服务; 修改按钮的事件 确认修改的提示窗口; ...

weixin_44540479的博客 341

c# c\s应用程序自动更新模块源码

c\s应用程序自动更新模块源码,可以直接编译 主要通过本地程序与服务端程序文件的版本号来升级与更新本地程序文件。 UpdateList.xml文件内容如下: Application autoUpdate http://10.0.5.98/SoftUpdate/ 2005-09-05 ItemSoft.exe . 1.0.0.0 说明: 程序的描述; 更新服务器地址,为一个虚拟目录或站点路径; 需要更新主程序文件,为exe; . 需要更新主程序文件所在路径; 1.0.0.0 主程序版本号; 需要更新的文件列表;Ver:文件版本号,Name:文件名,包括路径(相对); 使用时,在更新服务器上新建Web虚拟目录或站点,然后将需要更新的文件与UpdateList.xml放在上面; 本地将AutoUpdate.exe与UpdateList.xml放在主程序的根目录下。本地UpdateList.xml中的版本号如果小于服务端,自动程序会自动下载所需要更新的文件。 本人的完整.Net打包程序下载中所带的项目中,就使用了此更新程序。

5G边缘计算:核心网下沉革命

5G核心网与边缘计算的深度融合,通过UPF下沉、本地流量卸载等关键技术,实现了低时延、高效能的分布式计算架构。该技术将数据处理从核心网向边缘节点转移,支持工业控制、车联网、远程医疗等场景的1-10ms超低时延需求。5G核心网的SBA架构和CUPS设计为边缘计算提供了灵活部署基础,SMF、PCF等网元协同确保业务质量与可靠性。

Qinti_mm的博客 1399

[IT]C/S自动升级程序原理

背景 我们给几个景区做了一个C/S项目,但是由于需求的变化,或者是Bug的出现,我们不得不修改程序程序。众所周知,C/S结构的应用程序可维护性比较差,它不同于B/S结构的程序,可以随着服务器端的更新,立即显现出来。因此我们不得不经常跑去景区手动更换程序。这样非常的麻烦,因此我们就做了一个自动升级的程序,由景区自己下载更新。 运行形式 (1)是作为一种服务,用户开机后自动隐藏运行。这种方式技...

SmartSi 495

无人机巡检提速3倍!YOLOv8n+OpenVINO让Jetson Nano跑25FPS,还能多飞25分钟

摘要(149字): 针对无人机巡检中Jetson Nano算力不足导致的帧率低、首次推理延迟高、续航短等问题,提出YOLOv8n+OpenVINO联合优化方案: 模型轻量化:YOLOv8n参数量仅3.2M(较v8s减少71%),Jetson Nano帧率从8FPS提升至15FPS(原生),绝缘子裂纹召回率仅降3%; OpenVINO加速:AUTO模式动态分配CPU/GPU算力,首次推理延迟从500ms降至200ms,帧率再优化至25FPS; 能效提升:功耗降低40%,单次续航延长25分钟。方案已落地电力/林

专注于Python爬虫开发,分享爬虫技巧、项目实战与反爬经验,使用Scrapy、BeautifulSoup等工具,解决数据抓取难题。 1241

一种电子病历系统软件框架思想——B/S与C/S混合架构

电子病历系统到底采用B/S还是C/S架构,是一个长期争论的话题。而在业界,两种架构的应用范围谁也不占有显著优势。在此,笔者提出一种B/S和C/S混合的架构,以下是其原理图。 B/S和C/S混合架构原理图 在该架构中,主要包括如下几个部分: 1.Web服务器 这是系统的核心。大多数的业务流程运行在Web服务器中。采用ASP.NET开发,直连数据库。 Web服务器包含系统功能 API集合...

lnc2003的专栏 4542

lighttools用透镜旋转手动创建菲尼尔透镜

学过很多软件,都没有留下痕迹,很多年之后只记得学过的软件常用的一些功能,稍微难一些的再次用起需要重新学,但如果留下点痕迹,重新再用就会很快。lighttools无意之中接触了,记一点点笔记吧。...

kingsai2012的博客 4329

YOLOv8/v9 遥感图像目标检测实战指南:建筑物检测与变化监测

遥感图像在城市规划、灾害评估、农业监管等领域应用广泛,但传统目标检测方法存在效率低、精度不足、难以适配遥感图像大尺寸、多尺度目标等问题。本文以“建筑物检测与变化监测”为核心,基于DOTA、NWPU VHR-10等公开数据集,系统讲解YOLOv8/v9在遥感图像中的实战流程。从数据获取、格式转换、模型训练优化,到大图切片推理、建筑物变化监测,全程提供可复现的代码与参数配置。通过针对性优化(旋转增强、切片推理等),模型在NWPU VHR-10数据集上mAP50达0.85以上,可适配卫星影像、无人机航拍等多场景

专注于人工智能、软件开发、工控自动化、工厂数字化及智能化等领域,希望和大家共同进步! 519

智能客户端:C/S与B/S的融合

智能客户端:C/S与B/S的融合■ 娄激扬 C/S架构与B/S架构都存在不可避免的局限,而智能客户端恰好能克服它们存在的问题,提高用户满意度,降低总体拥有成本。 人们对智能客户端的关注开始于微软。事实上,自微软推出.NET战略以来,其最令人关注的技术新动向之一就是对智能客户

娟子博客专栏 欢迎您的加入 为我们的IT之路共同努力 1985

基于唤醒音频的目标说话人语音识别系统设计报告

摘要 本报告设计了一套基于唤醒音频的目标说话人语音识别系统,通过五级流水线架构(唤醒检测→声纹注册→语音分离→目标ASR→拒识决策)解决复杂声学场景下的目标说话人识别问题。系统整合ECAPA-TDNN声纹模型、Conv-TasNet语音分离网络和Whisper ASR引擎,在L20 GPU上实现高效端到端推理,支持-5dB~5dB SNR范围和0~100%重叠率的双人语音场景处理。系统在目标说话人CER、拒识率和推理效率等关键指标上均达到设计目标,为智能语音交互提供了有效的身份感知解决方案。

huanghm88的专栏 73

实现C/S程序的自动更新

最近在做一个.Net C/S的系统,需要实现自动更新。MS已经提供了ClickOnce,很方便,但是用起来不太习惯,还是决定自己写一个简单的。自动更新无非文件比较、下载、启动程序几个步骤,其中文件比较可以通过手动在配置文件中维护版本号,也可以比较文件的MD5值,或者在.Net里还可以用Assembly或文件的版本号。因为怕麻烦,手动维护不考虑,剩下两者各有所长,都提供了以供选择。下载就比较简单

danjiewu的专栏 1万+

ARM交叉编译gdbserver并远程在线调试

arm-linux平台。5、生成完整的makefile,新建full_install,执行 ./configure --target=arm-linux-gnueabihf --host=arm-linux-gnueabihf --program-prefix=arm-linux-gnueabihf- CC=arm-linux-gnueabihf-gcc --prefix=/home/geyuxi/Documents/full_install。如果--host和--build不同,是交叉编译;

wangjie36的博客 3166

仿真学习系列(三十八):Sigrity仿真之单端S参数提取

S 参数(Scattering Parameters)是高速信号分析中最基础也最常用的指标,能够全面表征走线、过孔、连接器等互连结构的高频行为。在 Sigrity PowerSI 中,Model Extraction 是最常用的 S 参数提取方式,适用于高速差分线、单端线、过孔、参考平面等各种拓扑结构。 本节将通过示例讲解如何在 PowerSI 中完成一次完整的 S 参数提取流程,包括:对象选择、端口设置、频率范围配置以及结果查看。掌握此方法后,即可对任意高速网络执行阻抗、插损、回损、串扰分析,为进一步的

案例文章,高速信号 837

C/S与B/S架构深度解析:从原理到实战选型指南

在软件工程领域,客户端/服务器(C/S)和浏览器/服务器(B/S)是两种最基础且核心的软件架构模型。C/S架构通过预先安装的客户端应用分担计算,能充分利用本地资源,提供出色的交互体验和离线能力,常见于对性能和系统集成要求高的专业软件与游戏。B/S架构则以浏览器为统一入口,实现了零部署、跨平台和集中式管理,成为互联网应用的主流,其技术栈涵盖丰富的前端框架与后端服务。理解两者的工作原理、技术价值与适用场景,是进行技术选型、平衡用户体验与运维成本的关键。本文通过对比分析与实战场景,深入探讨了包括Electron、

weixin_34224941的博客 353

复分析 阿尔福斯(Ahlfors) 第三版答案 英文

答案分为十章,有部分习题没有答案,全英文复分析阿尔福斯(Ahlfors) 第三版答案

C/S架构系统的自动更新功能设计与实现(五)

文章目录更新程序的自我升级思路代码脚本调用 更新程序的自我升级 上一章节讲到客户端的更新。现在面临一个新的问题,就是更新程序本身要实现更新,该怎么办呢? 思路 我们当然知道,更新程序无法在运行期间实现自我更新,要对其实现更新,必须先关闭程序,再进行更新。于是我们想到,在更新程序关闭后,在其回调启动的主程序中,实现对更新程序的自我升级。 简单来说,就是A调用B,来更新B,更新完成后,A关闭,并回调B...

a_dev的专栏 710

蓝牙模块HM-10手册

蓝牙模块HM-10手册,该手册对蓝牙模块HM-10进行了详细的介绍和使用方法以及一些AT指令。

ET2012新版免狗免安装绿色软件

ET2012新版免狗免安祖昂绿色软件。,新界面,新视觉,

C/S与B/S架构深度解析:从核心原理到实战选型指南

客户端/服务器(C/S)和浏览器/服务器(B/S)架构是软件工程中两种基础且核心的系统设计模式。C/S架构基于专用客户端与服务器通过高效二进制协议(如gRPC)通信,强调本地计算、强状态管理和离线能力,适用于对性能与实时性要求极高的场景。B/S架构则以浏览器为统一客户端,基于HTTP/HTTPS等标准Web协议实现跨平台访问与无缝更新,降低了部署和维护成本。从技术价值看,C/S在复杂图形处理、高频交易等专业领域性能优势明显,而B/S在快速迭代、跨平台兼容和用户体验一致性方面更具优势。随着WebAssembl

weixin_34007886的博客 1689

大中型超市收银系统(可直接运行)

光步前台收银系统系统功能:商品销售、商品退货、挂单、收银员权限、会员销售、单品取消、整单取消、商品查询、脱机销售、脱机数据上传,基本资料下载,开钱箱、小票打印、收款结算,支持多币种付款方式(现金、储值卡、礼券、微信等),支持多种促销。运行环境:.net 4.0登陆信息用户名:1001 密码:123456内置商品信息,商品编码可输入00001000 - 00010000,测试效果相关功能键F5 结算 F9挂单 F11商品查询 F8退货 Insert单品取消 Del整单取消 F12开钱箱F2 会员 Ctrl+Z退出系统 Home 锁屏 相关源码待公开

上一篇: C/S架构系统的自动更新功能设计与实现(三)
下一篇: C/S架构系统的自动更新功能设计与实现(五)
acngiser
博客等级 码龄15年 118粉丝 214原创
评论
成就一亿技术人!
拼手气红包6.0元
还能输入1000个字符
 
 条评论被折叠 查看
添加红包

请填写红包祝福语或标题

个

红包个数最小为10个

元

红包金额最低5元

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

抵扣说明:

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

余额充值