一、前言
前段时间一直在折腾基于 Socket 的产品在线升级模块。之前我曾写过基于. Net Remoting 的、基于 WCF 的在线升级功能,由于并发量较小及当时代码经验的不足一直没有实际应用。这次下定决心撰写基于 Socket 的在线更新功能,一方面是觉得 Socket 的并发量较高,另一方面也是自己工作了一年多,积攒了一定的经验,应该能 hold 住。本文将展示的是 Protype 版本,Release 版本已在远程测试服务器上运行,并发数过万没有什么问题,文件更新都很正常。代码的 Github 地址将在本文最后提供。本文将展示的在线更新功能模块涉及 Devexpress WPF、webapi、Windows Service,我会从最基础的开始说起,非常适合初入的新手,大牛或者老司机可直接略过。二、方案
公司的产品是运行在某一 BIM 软件上的插件,要想做在线更新,有以下两种方案:
方案一:
插件安装后会在客户桌面上生成一个快捷方式,双击快捷方式会启动一个 LaunchProduct.exe,在这里面进行更新操作,更新完之后再启动 BIM 软件。
方案二:
用户首先运行 BIM 软件,点击插件里的更新按钮,然后通过 Socket 下载文件。进程中 Kill 掉该 BIM 软件,执行文件替换,再自动启动该 BIM 软件。(不 kill 掉的话程序一直被占用是无法更新文件的)三、步骤详解
无论是方案一,还是方案二,有些核心步骤是不变的。下面详细论述:
Step1: 从注册表中读取当前产品的版本、安装位置等信息。注册表是在做产品安装包时就应该要做的一件事情。产品安装完就会在客户机生成相应的注册表信息。我正好也是做产品安装包的,非常熟悉产品注册表里有哪些内容。那么这里,我封装了一个读注册表的 class,可以在 Github 项目里找到:RegistryUtils (UpdaterClient 工程中)在这里要提醒的一个地方是:有时候注册表明明有内容,C# 代码调试却是 null,那么解决的办法如下:
- var localMachineRegistry = RegistryKey.OpenBaseKey(RegistryHive.LocalMachine, Environment.Is64BitOperatingSystem ? RegistryView.Registry64 : RegistryView.Registry32);
- //用这个localMachineRegistry去OpenSubKey()
- //而不是直接用Registry.LocalMachine去OpenSubKey()
Step2:从注册表里读取完本地产品的相关信息后,我把这些数据封装成一个对象,去请求产品服务器上的某个 Webapi。如果有更新文件,会返回给我更新文件的大小及 MD5 值。更新文件的大小决定了我每个分包的大小,更新文件的 MD5 值用于我下载完分包进行合并后进行 MD5 比对,验证下载的包是否完整。Md5Utils(UpdaterShare 工程中)
- /// <summary>
- /// Get Download File Info
- /// </summary>
- /// <param name="basicInfo"></param>
- /// <param name="serverAddress"></param>
- /// <param name="controllerName"></param>
- /// <param name="actionName"></param>
- /// <param name="serverResult"></param>
- /// <returns></returns>
- public static bool RequestDownloadFileInfo(ClientBasicInfo basicInfo,
- string serverAddress,
- string controllerName,
- string actionName,
- ref DownloadFileInfo serverResult)
- {
- var packageInfo = JsonConvert.SerializeObject(basicInfo);
- try
- {
- HttpClient httpClient = new HttpClient
- {
- BaseAddress = new Uri(serverAddress),
- Timeout = TimeSpan.FromMinutes(20)
- };
- if (ConnectionTest(serverAddress))
- {
- StringContent strData = new StringContent(packageInfo, Encoding.UTF8, "application/json");
- string postUrl = httpClient.BaseAddress + $"api/{controllerName}/{actionName}";
- Uri address = new Uri(postUrl);
- Task<HttpResponseMessage> task = httpClient.PostAsync(address, strData);
- try
- {
- task.Wait();
- }
- catch
- {
- return false;
- }
- HttpResponseMessage response = task.Result;
- if (!response.IsSuccessStatusCode)
- return false;
- try
- {
- string jsonResult = response.Content.ReadAsStringAsync().Result;
- serverResult = JsonConvert.DeserializeObject<DownloadFileInfo>(jsonResult);
- if (serverResult != null)
- {
- return true;
- }
- }
- catch(Exception ex)
- {
- return false;
- }
- }
- }
- catch
- {
- return false;
- }
- return false;
- }
APM 是微软比较早的提供用于 Socket 通信的方法。其最常见的写法就是 BeginAction(byte[] buffer, int offset, int size, SocketFlags socketFlags, AsyncCallback callback, object state),在 callback 回调函数里 EndAction。
我在 Client 里是通过生成 5 个 Task,每个 Task 各有一个 Socket 去下载 1/5 文件,最后合并。
那么有的线程接收文件快,有的接收文件慢。因为最后还要分别生成 5 个临时文件进行合并。所以需要一个线程同步,让快的或慢的都在终点线等着。这里就要用到 ManualResetEvent,其继承于 EventWaitHandle,EventWaitHandle 又继承于 WaitHandle。ManualResetEvent 是怎么使用的呢?因为代码都在 Github 上,这里提炼一下,总结就是: ManualResetEvent 初始为 false 的时候,只有在某个线程中使用 ManualResetEvent.Set() 方法,才能让另一线程中写在 ManualResetEvent.WaitOne() 之后的代码运行。假设主线程里调用了 WaitOne(),那么主线程写在 WaitOne 之后的代码要想执行,就必须等待子线程中调用 Set() 方法,否则主线程会一直阻塞在 WaitOne() 处。在 Socket 里肯定要定义一个自己产品的数据包格式,因为 Socket 里传的都是 byte[],你肯定要让客户端 / 服务器知道你发的 byte[] 是什么意思吧,所以要定义数据包格式:
- var tasks = new Task[packetCount];
- for (int index = 0; index < packetCount; index++) {
- int packetNumber = index;
- var task = new Task(() = >{
- Socket client = new Socket(AddressFamily.InterNetwork, SocketType.Stream, ProtocolType.Tcp);
- ComObject state = new ComObject {
- WorkSocket = client,
- PacketNumber = packetNumber
- };
- client.BeginConnect(remoteEp, ConnectCallback, state);
- });
- tasks[packetNumber] = task;
- task.Start();
- }
- Task.WaitAll(tasks);
- A Packet = start_tag + version_tag + request / response_tag + length_tag + data + crc16_tag
1. 包头标识,一般用 {0xAA, 0x55}
2. 格式版本,暂且定为 {0x01}
3. 发送标识,是客户端发的呢?还是服务器发的呢?
4. 长度标识,用于记录整个数据包的长度,此标识占 2 个字节
5. 数据,要传输的数据
6. crc16 校验码。用于判断传输的 byte[] 是否完整,相对于 MD5,crc16 校验码的字节数更短,不会占太多传输字节,非常适合用于字节数组的比较。MD5 常常用于文件对比。
那么,有了长度标识与 crc16 校验码的双保险,我们就可以知道传输的 byte[] 是否完整了。
当一方发送 byte[] 后,另一方收到后可以拿出长度标识,判断 byte[] 长度是否正确;当长度正确后,使用 Crc16Utils 计算收到的 byte[] 的 crc16 码并与 byte[] 中的 crc16 码进行比对。
Step4:服务器端的 Socket 是写在 Windows Service 里的。更新文件就放在该 Windows Service 同路径下,因为 Windows Service 在启动运行之后会在注册表写下相应信息,通过注册表就能知道该 Windows Service 的执行路径,继而得到更新文件的路径。
Step5: 当下载完文件,其实已经完成了 90% 的工作了,剩下的无非就是简单的替换文件,更新注册表信息等等。
- /// <summary>
- /// Get Latest File From Windows Service by Registry
- /// </summary>
- /// <param name="serviceName"></param>
- /// <returns></returns>
- public static string GetFilePathFromService(string serviceName)
- {
- try
- {
- ServiceController[] services = ServiceController.GetServices();
- var socketService = services.FirstOrDefault(x => String.Equals(x.ServiceName, "SocketService"));
- if (socketService != null)
- {
- var localMachineRegistry = RegistryKey.OpenBaseKey(RegistryHive.LocalMachine, Environment.Is64BitOperatingSystem ?
- RegistryView.Registry64 : RegistryView.Registry32);
- var key = localMachineRegistry.OpenSubKey(@"SYSTEM\CurrentControlSet\Services\" + serviceName);
- if (key != null)
- {
- var serviceExePath = GetString(key.GetValue("ImagePath").ToString());
- var folderPath = Path.GetDirectoryName(serviceExePath);
- if (!String.IsNullOrEmpty(folderPath) && Directory.Exists(folderPath))
- {
- return folderPath;
- }
- }
- }
- }
- catch(Exception ex)
- {
- return null;
- }
- return null;
- }
最后附上完整的流程图:
四、其它
我在写代码的时候参考了微软的示例,还是非常有帮助的,在此提供下:
Microsoft 官方示例:
目前微软早已提供了更接近 Socket 底层的 SocketAsyncEventArgs(SAEA)写法,该方法不同于 APM 的是:
1. APM 多次 Send\Receive 会产生多个 IAsyncResult 对象,增加消耗。
2. SAEA 配合 BufferManager 以及池化能很好的调配服务器资源,有多少坑就蹲多少人,再多了就可以考虑转移至其它服务器做均衡了。
3. SAEA 的并发能力比 APM 略高,但是坑也不少,比如 APM 中,通过 EndReceive 是否为 0 我就能知道还有没有数据要接收,但是 SAEA 中的 Avilable 等于 0 时还可能有数据没接收完,这个问题的解决方法网上各种各样,各位可以自己搜搜。SAEA 的服务器写法我看看之后有没有时间写写。GitHub 地址: https://github.com/airforce094/SocketUpdater 五、最后
此 Github 里涉及 Devexpress WPF、Webapi、Windows Service,不求 Star,您的阅读就是对我最大的支持。有什么问题可留言相互讨论。今年由于公司管理层变动,人员及业务会有较大调整,突然觉得小公司有时候也挺折腾的。我继续淡定学车,坐看其变。《原创,转载请注明来源》
来源: http://www.cnblogs.com/lovecsharp094/p/8087605.html