【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

相關文章
相關標籤/搜索