一步一步開發Game伺服器(三)載入指令碼和伺服器熱更新
大家可能對遊戲伺服器的執行不太理解或者說不太清楚一些機制。
但是大家一定會明白一點,當程式在執行的時候出現一些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目的。