大家可能對游戲服務器的運行不太理解或者說不太清楚一些機制。
但是大家一定會明白一點,當程序在運行的時候出現一些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目的。