一、前言

      前段时间一直在折腾基于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()
}; 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;
}

Step3: 拿到更新文件的大小及MD5值后,我们就可以开始本地通过Socket去请求服务器下载更新文件了。这里的Socket我使用的是APM写法,也就是异步编程模型。

APM是微软比较早的提供用于Socket通信的方法。其最常见的写法就是 BeginAction(byte[] buffer, int offset, int size, SocketFlags socketFlags, AsyncCallback callback, object state),在callback回调函数里EndAction。

我在Client里是通过生成5个Task,每个Task各有一个Socket去下载1/5文件,最后合并。

           var tasks = new Task[packetCount];
for (int index = ; 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);

那么有的线程接收文件快,有的接收文件慢。因为最后还要分别生成5个临时文件进行合并。所以需要一个线程同步,让快的或慢的都在终点线等着。这里就要用到 ManualResetEvent,其继承于EventWaitHandleEventWaitHandle又继承于WaitHandleManualResetEvent是怎么使用的呢?因为代码都在Github上,这里提炼一下,总结就是: ManualResetEvent 初始为false的时候,只有在某个线程中使用ManualResetEvent.Set()方法,才能让另一线程中写在ManualResetEvent.WaitOne()之后的代码运行。假设主线程里调用了WaitOne(),那么主线程写在WaitOne之后的代码要想执行,就必须等待子线程中调用Set()方法,否则主线程会一直阻塞在WaitOne()处。

在Socket里肯定要定义一个自己产品的数据包格式,因为Socket里传的都是byte[],你肯定要让客户端/服务器知道你发的byte[]是什么意思吧,所以要定义数据包格式:

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的执行路径,继而得到更新文件的路径。

        /// <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;
}

Step5: 当下载完文件,其实已经完成了90%的工作了,剩下的无非就是简单的替换文件,更新注册表信息等等。

最后附上完整的流程图:

、其它

我在写Socket代码的时候参考了微软的示例,还是非常有帮助的,建议先看微软的示例再看Github的代码会更方便理解,在此提供下:

Microsoft官方示例:

https://docs.microsoft.com/en-us/dotnet/framework/network-programming/asynchronous-client-socket-example

目前微软早已提供了更接近Socket底层的SocketAsyncEventArgs(SAEA)写法,该方法不同于APM的是:

1. APM多次Send\Receive会产生多个IAsyncResult对象,增加消耗。

2. SAEA配合BufferManager以及池化能很好的调配服务器资源,有多少坑就蹲多少人,再多了就可以考虑转移至其它服务器做均衡了。

3. SAEA的并发能力比APM略高,但是坑也不少,比如APM中,通过EndReceive是否为0我就能知道还有没有数据要接收,但是SAEA中的Available等于0时还可能有数据没接收完,这个问题的解决方法网上各种各样,各位可以自己搜搜。SAEA的服务器写法我看看之后有没有时间写写。

GitHub地址:https://github.com/airforce094/SocketUpdater

、最后

此Github里涉及Devexpress WPF、Webapi、Windows Service,不求Star,您的阅读就是对我最大的支持。有什么问题可留言相互讨论。

《原创,转载请注明来源》

来自:airforce094

C#中级-从零打造基于Socket在线升级模块的更多相关文章

  1. 【Socket】从零打造基于Socket在线升级模块

    一.前言       前段时间一直在折腾基于Socket的产品在线升级模块.之前我曾写过基于.Net Remoting的.基于WCF的在线升级功能,由于并发量较小及当时代码经验的不足一直没有实际应用. ...

  2. Python之基于socket和select模块实现IO多路复用

    '''IO指的是输入输出,一部分指的是文件操作,还有一部分网络传输操作,例如soekct就是其中之一:多路复用指的是利用一种机制,同时使用多个IO,例如同时监听多个文件句柄(socket对象一旦传送或 ...

  3. 在线白板,基于socket.io的多人在线协作工具

    首发:个人博客,更新&纠错&回复 是昨天这篇博文留的尾巴,socket.io库的使用练习,成品地址在这里. 代码已经上传到github,传送门.可以开俩浏览器看效果. 现实意义是俩人在 ...

  4. 基于socket.io的实时在线选座系统

    基于socket.io的实时在线选座系统(demo) 前言 前段时间公司做一个关于剧院的项目,遇到了这样一种情况. 在高并发多用户同时选座的情况下,假设A用户进入选座页面,正在选择座位,此时还没有提交 ...

  5. 基于Socket客户端局域网或广域网内共享同一短信猫收发短信的开发解决方案

    可使同一网络(局域网或广域网)内众多客户端,共享一个短信猫设备短信服务器进行短信收发,短信服务器具备对客户端的管理功能. 下面是某市建设银行采用本短信二次开发平台时实施的系统方案图: 在该方案中,考虑 ...

  6. Android 基于Socket的聊天应用(二)

    很久没写BLOG了,之前在写Android聊天室的时候答应过要写一个客户(好友)之间的聊天demo,Android 基于Socket的聊天室已经实现了通过Socket广播形式的通信功能. 以下是我写的 ...

  7. c#编写的基于Socket的异步通信系统

    c#编写的基于Socket的异步通信系统 SanNiuSignal是一个基于异步socket的完全免费DLL:它里面封装了Client,Server以及UDP:有了这个DLL:用户不用去关心心跳:粘包 ...

  8. 从零打造在线网盘系统之Struts2框架起步

    欢迎浏览Java工程师SSH教程从零打造在线网盘系统系列教程,本系列教程将会使用SSH(Struts2+Spring+Hibernate)打造一个在线网盘系统,本系列教程是从零开始,所以会详细以及着重 ...

  9. 从零打造在线网盘系统之Struts2框架配置全解析

    欢迎浏览Java工程师SSH教程从零打造在线网盘系统系列教程,本系列教程将会使用SSH(Struts2+Spring+Hibernate)打造一个在线网盘系统,本系列教程是从零开始,所以会详细以及着重 ...

随机推荐

  1. 定制rpm包---Yum环境搭建

    1.1 在yum服务器上创建yum仓库命令 mkdir -p /application/nginx/html/yum cd /application/nginx/html/yum rz #上传rpm包 ...

  2. linux的特殊符号与正则表达式

    第1章 linux的特殊符号 1.1 通配符 * {} 1.1.1 含义 方便查找文件 通配符是用来找文件名字的. 1.1.2  * 通过find 命令找以 .sh 结尾的文件,使用*替代文件名字. ...

  3. Azure ASM虚拟机部署“安全扩展”

    Azure虚拟机,默认情况下没有安装杀毒软件.如果您有此需求可以通过Azure 扩展进行安装,有关Azure反恶意软件的官方说明请参考:https://docs.azure.cn/zh-cn/secu ...

  4. 在centos上安装jenkins

    摘要: 本篇介绍了如何在linux服务器上安装jenkins 一:使用war安装 官网地址:https://jenkins.io/doc/ Guided Tour This guided tour w ...

  5. web前端学习路线和步骤

    H5+全栈工程师  (学习下列技术可以加QQ: 1416 7596 61)第一阶段:初级入门阶段基本功 1.HTML入门 Windows概述.浏览器概述.HTML简介.HTML标签详解.前端开发工具概 ...

  6. Java微信公众平台开发_06_素材管理

    一.本节要点 1.官方文档的media 这个media可以理解为文件,即我们需要以POST方式提交一个文件 2.媒体文件有效期 媒体文件在微信后台保存时间为3天,即3天后media_id失效. 二.代 ...

  7. 2017阿里云双11-云服务器ECS优惠活动最强解读和购买指南

    本站之前介绍了<爆款云服务器,限时2折起>,这其实是阿里云双11之前的预热活动:四款低配的机型,二折给用户(每个用户限购一台),非常的实惠,有很多阅读了本站文章的用户都一次性购买了三年的. ...

  8. 数据库之Oracle——初级

    世上岂无千里马,人中难得九方皋: 酒船鱼网归来是,花落故溪深一篙. 关于数据库的第一篇博客,这是我的第二次,人生第二春,什么也不想说,静静的开始吧,至于为什么写唐诗,请看第一篇文章! Oracle 初 ...

  9. CLR类型设计之方法与构造器

    无论学习那门语言都要学习函数体,C#,JAVA,PHP,都会涉及到函数体,而C#的函数体成员并不少,方法和构造器就是函数体成员之一,函数体成员还包括但不限于:方法,属性,构造器,终结器,运算符及索引器 ...

  10. TensorFlow(三)---------正则化

    TensorFlow正则化经常被用于Deep-Learn中,泛化数据模型,解决过拟合问题.再深度学习网络只有在有足够大的数据集时才能产生惊人的学习效果.当数据量不够时,过拟合的问题就会经常发生.然而, ...