大家可能對游戲服務器的運行不太理解或者說不太清楚一些機制。
但是大家一定會明白一點,當程序在運行的時候出現一些bug,必須及時更新,但是不能重啟程序的情況下。
這裡牽涉到一個問題。比如說在游戲裡面,,如果一旦開服,錯非完全致命性bug,否則是不能頻繁重啟服務器程序的,
你重啟一次就可能流失一部分玩家。那麼就牽涉到程序熱更新修復bug功能。
今天就來扒一扒熱更新的事情。
java和C#的加載機制有著一定的區別,java是吧.java的文件編譯成.class的文件進行加載的。而c#是把.cs的相關文件打包成DLL才能進行加載。
這樣導致的結果就是,java可以熱更新單個.class文件 而C#就只能做到加載DLL文件。
至於java的加載機制和代碼我就不在BB了,以後會發表相關文章。
今天只關注C#如何做到就行。
我們創建一個類庫項目 ClassLibraryMain
創建類 TestMain
public class TestMain { public static string TestStr = "ssss"; }
創建兩個接口
public interface IScript2 { } public interface IScript { string GetStr(); }
創建類庫 ClassLibraryScript 然後添加引用 ClassLibraryMain
創建類 TestScript1
public class TestScript1 : IScript, IScript2 { public string GetStr() { return "我是《TestScript1》" + TestMain.TestStr; } }
創建類 TestScript
public class TestScript : IScript { public TestScript() { } public string GetStr() { return "我是《TestScript》" + TestMain.TestStr; } }
創建一個解決方案文件夾 NewFolder1 在創建類 TestScript
public class TestScript : IScript { public TestScript() { } public string GetStr() { return "我是《ClassLibraryScript.NewFolder1.TestScript》" + TestMain.TestStr; } }
准備工作完成,接下來分析一下C#的加載
C#下動態加載類,那麼需要利用System.Reflection 空間下面的反射,才能完成對DLL的加載
Assembly 對象,是反射。
Assembly.LoadFrom(string path);//加載DLL或者EXE程序
Assembly.GetExportedTypes();獲取程序集中所有的類型,
Type.GetInterfaces();獲取一個類型的所有繼承和實現的接口對象;
創建 LoadScriptManager 類
1 /// <summary> 2 /// 只支持加載一個DLL, 3 /// </summary> 4 public class LoadScriptManager 5 { 6 private static readonly LoadScriptManager instance = new LoadScriptManager(); 7 public static LoadScriptManager GetInstance { get { return instance; } } 8 9 private Dictionary<string, List<object>> Instances = new Dictionary<string, List<object>>(); 10 11 /// <summary> 12 /// 13 /// </summary> 14 /// <param name="pathName">文件路徑,包含名稱。dll, exe</param> 15 public void Load(string pathName) 16 { 17 GC.Collect(); 18 Assembly assembly = Assembly.LoadFrom(pathName); 19 Type[] instances = assembly.GetExportedTypes(); 20 Dictionary<string, List<object>> tempInstances = new Dictionary<string, List<object>>(); 21 foreach (var itemType in instances) 22 { 23 #if DEBUG 24 Console.Write(itemType.Name); 25 #endif 26 Type[] interfaces = itemType.GetInterfaces(); 27 object obj = Activator.CreateInstance(itemType); 28 foreach (var iteminterface in interfaces) 29 { 30 #if DEBUG 31 Console.Write(": " + iteminterface.Name); 32 #endif 33 if (!tempInstances.ContainsKey(iteminterface.Name)) 34 { 35 tempInstances[iteminterface.Name] = new List<object>(); 36 } 37 tempInstances[iteminterface.Name].Add(obj); 38 } 39 #if DEBUG 40 Console.WriteLine(); 41 #endif 42 } 43 lock (Instances) 44 { 45 Instances = tempInstances; 46 } 47 } 48 49 /// <summary> 50 /// 根據名稱查找實例 51 /// </summary> 52 /// <param name="name"></param> 53 /// <returns></returns> 54 public List<object> GetInstances(string name) 55 { 56 lock (Instances) 57 { 58 if (Instances.ContainsKey(name)) 59 { 60 return new List<object>(Instances[name]); 61 } 62 } 63 return null; 64 } 65 }
接下來我們測試一下,
創建一個控制台程序,然後添加引用 ClassLibraryMain 把ClassLibraryScript的DLL文件拷貝到控制台程序的DEBUG目錄下面,或者其他目錄,我是放在DEBUG目錄下的
1 class Program 2 { 3 static void Main(string[] args) 4 { 5 GC.Collect(); 6 LoadScriptManager.GetInstance.Load("ClassLibraryScript.dll"); 7 List<object> instances = LoadScriptManager.GetInstance.GetInstances(typeof(IScript).Name); 8 if (instances != null) 9 { 10 foreach (var item in instances) 11 { 12 if (item is IScript) 13 { 14 Console.WriteLine(((IScript)item).GetStr()); 15 } 16 } 17 } 18 Console.ReadLine(); 19 } 20 }
輸出:
TestScript: IScript
TestScript: IScript
TestScript1: IScript: IScript2
我是《ClassLibraryScript.NewFolder1.TestScript》ssss
我是《TestScript》ssss
我是《TestScript1》ssss
為了得到熱更新效果,我們修改一下程序
1 class Program 2 { 3 static void Main(string[] args) 4 { 5 while (true) 6 { 7 GC.Collect(); 8 TestMain.TestStr = Console.ReadLine(); 9 LoadScriptManager.GetInstance.Load("ClassLibraryScript.dll"); 10 List<object> instances = LoadScriptManager.GetInstance.GetInstances(typeof(IScript).Name); 11 if (instances != null) 12 { 13 foreach (var item in instances) 14 { 15 if (item is IScript) 16 { 17 Console.WriteLine(((IScript)item).GetStr()); 18 } 19 } 20 } 21 } 22 Console.ReadLine(); 23 } 24 }
第一次加載
TestScript: IScript
TestScript: IScript
TestScript1: IScript: IScript2
我是《ClassLibraryScript.NewFolder1.TestScript》第一次加載
我是《TestScript》第一次加載
我是《TestScript1》第一次加載
當我們嘗試去更新文件才發現,根本沒辦法更新,
如何解決文件的獨占問題呢?
查看 Assembly 發現一個可以使用字節流數組加載對象,
接下來修改一下 load方法
1 public void Load(string pathName) 2 { 3 Dictionary<string, List<object>> tempInstances = new Dictionary<string, List<object>>(); 4 try 5 { 6 GC.Collect(); 7 byte[] bFile = null; 8 using (FileStream fs = new FileStream(pathName, FileMode.Open, FileAccess.Read)) 9 { 10 using (BinaryReader br = new BinaryReader(fs)) 11 { 12 bFile = br.ReadBytes((int)fs.Length); 13 Assembly assembly = Assembly.Load(bFile); 14 Type[] instances = assembly.GetExportedTypes(); 15 foreach (var itemType in instances) 16 { 17 #if DEBUG 18 Console.Write(itemType.Name); 19 #endif 20 Type[] interfaces = itemType.GetInterfaces(); 21 object obj = Activator.CreateInstance(itemType); 22 foreach (var iteminterface in interfaces) 23 { 24 #if DEBUG 25 Console.Write(": " + iteminterface.Name); 26 #endif 27 if (!tempInstances.ContainsKey(iteminterface.Name)) 28 { 29 tempInstances[iteminterface.Name] = new List<object>(); 30 } 31 tempInstances[iteminterface.Name].Add(obj); 32 } 33 #if DEBUG 34 Console.WriteLine(); 35 #endif 36 } 37 } 38 } 39 } 40 catch (Exception ex) 41 { 42 Console.WriteLine("加載文件拋錯" + ex); 43 } 44 Instances = tempInstances; 45 }
運行一下效果
第一次
TestScript: IScript
TestScript: IScript
TestScript1: IScript: IScript2
我是《ClassLibraryScript.NewFolder1.TestScript》第一次
我是《TestScript》第一次
我是《TestScript1》第一次
接下來我們修改一下 TestScript1 腳本文件
public class TestScript1 : IScript, IScript2 { public string GetStr() { return "我是《TestScript1》 我是修改過後的 " + TestMain.TestStr; } }
然後編譯生成一次
這下就看到了,我們程序熱更新了,,
需要注意的是,C#依然可以做到更新單個文件,但是都必須打包成DLL,和java更新單個文件必須編譯成.class文件一樣。
目前,這個方式,實現的加載dll腳本,。但是沒有做加載後dll動態數據保存。這個比較復雜。
我們這裡的創建了三個項目,分別為, ConsoleApplication5 控制台, ClassLibraryMain 類庫 ClassLibraryScript 類庫,
引用關系為,ConsoleApplication5和ClassLibraryScript 引用了ClassLibraryMain 類庫,
ClassLibraryScript 可以調用 ClassLibraryMain 庫中保存的數據,
ClassLibraryScript 類庫僅僅是腳本。也就是說,通常可以把業務邏輯處理模塊獨立到這個庫中,完成業務邏輯。不牽涉數據保存。
這樣就能完全滿足程序的熱更新,不必重啟程序,達到了修改邏輯bug目的。