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

1、前言git

      前段时间一直在折腾基于Socket的产品在线升级模块。以前我曾写过基于.Net Remoting的、基于WCF的在线升级功能,因为并发量较小及当时代码经验的不足一直没有实际应用。此次下定决心撰写基于Socket的在线更新功能,一方面是以为Socket的并发量较高,另外一方面也是本身工做了一年多,积攒了必定的经验,应该能hold住。本文将展现的是Protype版本,Release版本已在远程测试服务器上运行,并发数过万没有什么问题,文件更新都很正常。代码的Github地址将在本文最后提供。本文将展现的在线更新功能模块涉及Devexpress WPF、Webapi、Windows Service,我会从最基础的开始提及,很是适合初入的新手,大牛或者老司机可直接略过。github

 

2、方案express

        公司的产品是运行在某一BIM软件上的插件,要想作在线更新,有如下两种方案:编程

        方案一:json

        插件安装后会在客户桌面上生成一个快捷方式,双击快捷方式会启动一个LaunchProduct.exe,在这里面进行更新操做,更新完以后再启动BIM软件。api

        方案二:数组

        用户首先运行BIM软件,点击插件里的更新按钮,而后经过Socket下载文件。进程中Kill掉该BIM软件,执行文件替换,再自动启动该BIM软件。(不kill掉的话程序一直被占用是没法更新文件的)服务器

 

3、步骤详解并发

       不管是方案一,仍是方案二,有些核心步骤是不变的。下面详细论述:app

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

 

 


 

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 = 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);

 

那么有的线程接收文件快,有的接收文件慢。由于最后还要分别生成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

相关文章
相关标签/搜索