一、前言

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

【Socket】从零打造基于Socket在线升级模块的更多相关文章

  1. C#中级-从零打造基于Socket在线升级模块

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

  2. 轨迹系列——Socket总结及实现基于TCP或UDP的809协议方法

    文章版权由作者李晓晖和博客园共有,若转载请于明显处标明出处:http://www.cnblogs.com/naaoveGIS/ 1.背景 在上一篇博客中我详细介绍了809协议的内容.809协议规范了通 ...

  3. 轨迹系列7——Socket总结及实现基于TCP或UDP的809协议方法

    文章版权由作者李晓晖和博客园共有,若转载请于明显处标明出处:http://www.cnblogs.com/naaoveGIS/ 1.背景 在上一篇博客中我详细介绍了809协议的内容.809协议规范了通 ...

  4. Node.js下基于Express + Socket.io 搭建一个基本的在线聊天室

    一.聊天室简单介绍 采用nodeJS设计,基于express框架,使用WebSocket编程之 socket.io机制.聊天室增加了 注册登录模块 ,并将用户个人信息和聊天记录存入数据库. 数据库采用 ...

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

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

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

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

  7. 利用socket.io+nodejs打造简单聊天室

    代码地址如下:http://www.demodashi.com/demo/11579.html 界面展示: 首先展示demo的结果界面,只是简单消息的发送和接收,包括发送文字和发送图片. ws说明: ...

  8. 【WCF】基于WCF的在线升级

    一.前言       前不久因公司产品需要完成了在线升级功能,因为编程技术不精,不敢冒然采用Socket方法实现在线升级,所以使用比较方便稳妥的WCF方式 如果考虑并发能力的话还是Socket> ...

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

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

随机推荐

  1. Drupal views 中合并显示字段

    如图, 需要显示如下数据表格 表格的第三列是由两个字段组成的.分别是title 标题字段和body 内容字段. 默认情况下,一列只显示一个字段.如何同时显示两个呢? 这个问题难不到强大的views,要 ...

  2. protobuf工程的编译以及使用

    一. 获取Protocol Buffer 1.1 获得源码 Github:ProtocolBuffer源码 Git clone之:git clone https://github.com/google ...

  3. Karma与TSLint

    TSLint TSLint是一个可扩展的静态分析工具,用于检查TypeScript代码的可读性,可维护性和功能性错误.收到现代编辑和构建系统的广泛支持,并且可以使用您自己的路由,配置和格式化. 安装 ...

  4. 个人安装ss的一个记录

    在ubuntu16.04安装ss服务.由于lantern最近极其不稳定(我还花钱的qaq),经常断联以至于几乎废了,莫得办法,只好花钱搭一个了orz...呵,贫穷.... 安装shadowsocks ...

  5. .net mvc5 不同view()的视图 代码

    public class Test { public int id { set; get; } public string name { set; get; } } public ActionResu ...

  6. drupal CVE-2018-7600 复现

    1.系统环境 Drupal 8.5 linux 主机 ruby 代码 2.原理说明 影响版本 Drupal 6.x,7.x,8.x 参考:CVE-2018-7600漏洞分析 3.利用 在Python2 ...

  7. how to update product listing price sale price and sale date using mobile App

    Greetings from Amazon Seller Support, Thank you for writing back to us. I have reviewed our previous ...

  8. mysql 设置远程登录

    1.本机登录进mysql,并切换到本机mysql数据库下 2. GRANT ALL PRIVILEGES ON *.* TO 'tigase'@'%' IDENTIFIED BY '123456' W ...

  9. 第八,九周web制作代码

      <!DOCTYPE html PUBLIC "-//W3C//DTD XHTML 1.0 Transitional//EN" "http://www.w3.or ...

  10. Java多线程中的wait与notify

    一.wait: 1. wait 是 object 类的方法, sleep 是 thread 类的方法. 2. 当前的正在我这个对象访问的线程 wait. 3. 当前的这个线程, 锁定在当前对象的这个线 ...