1. 程式人生 > >一步一步開發Game伺服器(三)載入指令碼和伺服器熱更新(二)完整版

一步一步開發Game伺服器(三)載入指令碼和伺服器熱更新(二)完整版

可是在使用過程中,也許有很多會發現,動態載入dll其實不方便,應為需要預先編譯程式碼為dll檔案。便利性不是很高。

那麼有麼有辦法能做到動態實時更新呢????

官方提供了這兩個物件,動態編譯原始檔。

提供對 C# 程式碼生成器和程式碼編譯器的例項的訪問。 CSharpCodeProvider
提供一下方法載入原始檔,


// 基於包含在 System.CodeDom.CodeCompileUnit 物件的指定陣列中的 System.CodeDom 樹,使用指定的編譯器設定編譯程式集。
public virtual CompilerResults CompileAssemblyFromDom(CompilerParameters options, params
CodeCompileUnit[] compilationUnits); // 從包含在指定檔案中的原始碼,使用指定的編譯器設定編譯程式集。 public virtual CompilerResults CompileAssemblyFromFile(CompilerParameters options, params string[] fileNames); // 從包含原始碼的字串的指定陣列,使用指定的編譯器設定編譯程式集。 public virtual CompilerResults CompileAssemblyFromSource(CompilerParameters options, params
string[] sources);

上面的方法我測試了CompileAssemblyFromFile  CompileAssemblyFromSource 兩個方法,

CompileAssemblyFromFile  的意思給定檔案路徑去編譯原始檔,可以直接加入除錯資訊,除錯,

CompileAssemblyFromSource 的意思給定原始碼類容去編譯原始檔,無法直接加入除錯資訊,需要加入  System.Diagnostics.Debugger.Break(); 在原始檔插入斷點除錯。但是在除非斷點的時候會彈出對話方塊,跳轉指定原始檔附近才能除錯。略微麻煩。

以上兩種方法需要除錯都需要下面的除錯引數配合 IncludeDebugInformation = true; 才能有用

表示用於呼叫編譯器的引數。 CompilerParameters
提供一下引數

//不輸出編譯檔案
parameter.GenerateExecutable = false;
//生成除錯資訊
parameter.IncludeDebugInformation = true;
//不輸出到記憶體
parameter.GenerateInMemory = false;
//新增需要的程式集
parameter.ReferencedAssemblies.AddRange(tempDllNames);

廢話不多說,我們來測試一下程式碼

首先建立一個 LoadManager.cs

public class LoadManager
    {
        private static LoadManager instance = new LoadManager();
        public static LoadManager GetInstance { get { return instance; } }

        public void LoadFile(string path)
        {
            CSharpCodeProvider provider = new CSharpCodeProvider();
            CompilerParameters parameter = new CompilerParameters();
            //不輸出編譯檔案
            parameter.GenerateExecutable = false;
            //生成除錯資訊
            parameter.IncludeDebugInformation = true;
            //不輸出到記憶體
            parameter.GenerateInMemory = false;
            //新增需要的程式集
            parameter.ReferencedAssemblies.Add("System.dll");
            //編譯檔案
            Console.WriteLine("動態載入檔案:" + path);
            CompilerResults result = provider.CompileAssemblyFromFile(parameter, path);//根據制定的檔案載入指令碼
            //判斷是否有錯誤
            if (!result.Errors.HasErrors)
            {
                //獲取載入的所有物件模型
                Type[] instances = result.CompiledAssembly.GetExportedTypes();
                foreach (var itemType in instances)
                {
                    Console.WriteLine("生成例項:" + itemType.Name);
                    //生成例項
                    object obj = Activator.CreateInstance(itemType);
                }
            }
            else
            {
                var item = result.Errors.GetEnumerator();
                while (item.MoveNext())
                {
                    Console.WriteLine("動態載入檔案出錯了!" + item.Current.ToString());
                }
            }
        }
    }

建立測試原始檔 TestCode.cs

public class TestCode
    {
        public TestCode()
        {
            Console.WriteLine("我是TestCode");
        }
    }

呼叫測試

string cspath = @"F:\javatest\ConsoleApplication6\CodeDomeCode\TestCode.cs";
            LoadManager.GetInstance.LoadFile(cspath);

輸出結果 表面我們載入成功了,

動態載入檔案:F:\javatest\ConsoleApplication6\CodeDomeCode\TestCode.cs
生成例項:TestCode
我是TestCode

接下來我們

修改一下 TestCode.cs

using System;
using System.Collections.Generic;
using System.Linq;
using System.Text;

namespace CodeDomeCode
{
    public class TestCode
    {
        public TestCode()
        {
            Console.WriteLine("我是TestCode");
        }
    }
}

結果程式輸出錯誤

動態載入檔案:F:\javatest\ConsoleApplication6\CodeDomeCode\TestCode.cs
動態載入檔案出錯了!f:\javatest\ConsoleApplication6\CodeDomeCode\TestCode.cs(3,14) : error CS0234: 名稱空間“System”中不存在型別或名稱空間名稱“Linq”(是否缺少程式集引用?)

這就出現了一個問題,

//新增需要的程式集
            parameter.ReferencedAssemblies.Add("System.dll");

我們的編譯引數。附件編譯依賴程式集的只添加了 System.dll 檔案,所有導致編譯出錯。

那麼我們知道思考一個問題,這個依賴程式集,必須要手動新增嘛?是不是太費事 ?

如果是做公共模組的話。我這麼知道需要哪些依賴程式集呢?

系統提供了AppDomain.CurrentDomain.GetAssemblies();獲取當前程式集所有程式集

Assembly.GetModules();程式集依賴項;

既然這樣,我們是不是可以依賴當前應用程式域載入呢?

修改一下依賴程式集新增方式

HashSet<String> ddlNames = new HashSet<string>();
            var asss = AppDomain.CurrentDomain.GetAssemblies();
            foreach (var item in asss)
            {
                foreach (var item222 in item.GetModules(false))
                {
                    ddlNames.Add(item222.FullyQualifiedName);
                }
            }

            //新增需要的程式集
            parameter.ReferencedAssemblies.AddRange(ddlNames.ToArray());

編譯完成,依賴於依賴當前應用程式域載入依賴程式集;(需要注意的時候你的程式碼裡面依賴的程式集,當前應用程式域也需要載入)

動態載入檔案:F:\javatest\ConsoleApplication6\CodeDomeCode\TestCode.cs
生成例項:TestCode
我是TestCode

接下來我們看看如何才能加入除錯情況呢?

有兩個問題,如果要加入除錯,需要修改兩個引數才能加入斷點除錯

//生成除錯資訊
parameter.IncludeDebugInformation = true;
//輸出編譯物件到記憶體
parameter.GenerateInMemory = true;

在程式碼中直接加入斷點測試

執行起來

進入斷點除錯了。

如果是原始檔是文字檔案但是需要加入除錯的話;

System.Diagnostics.Debugger.Break();

我們看到加入了除錯了,兩種方式都能加入除錯資訊;

問題繼續出現,我們在載入原始檔的時候,需求裡面肯定存在更新所載入的原始檔吧。

而且載入的檔案物件肯定需要儲存提供呼叫;

修改程式。

使用執行緒安全的集合儲存所載入的例項物件

 ConcurrentDictionary<string, ConcurrentDictionary<string, object>> Instances = new ConcurrentDictionary<string, ConcurrentDictionary<string, object>>();
//獲取載入的所有物件模型
            Type[] instances = assembly.GetExportedTypes();
            foreach (var itemType in instances)
            {
                //獲取單個模型的所有繼承關係和介面關係
                Type[] interfaces = itemType.GetInterfaces();
                //生成例項
                object obj = Activator.CreateInstance(itemType);
                foreach (var iteminterface in interfaces)
                {
                    //判斷是否存在鍵
                    if (!Instances.ContainsKey(iteminterface.Name))
                    {
                        Instances[iteminterface.Name] = new ConcurrentDictionary<string, object>();
                    }
                    //無加入物件,有更新物件
                    Instances[iteminterface.Name][itemType.Name] = obj;
                }
            }

把物件加入到集合中

/// <summary>
        /// 返回查詢的指令碼例項
        /// </summary>
        /// <typeparam name="T">型別</typeparam>
        /// <returns></returns>
        public IEnumerable<T> GetInstances<T>()
        {
            //使用列舉迭代器,避免了再一次建立物件
            string typeName = typeof(T).Name;
            if (Instances.ContainsKey(typeName))
            {
                foreach (var item in Instances[typeName])
                {
                    if (item.Value is T)
                    {
                        yield return (T)item.Value;
                    }
                }
            }
        }

最後附加全套原始碼

提供 原始檔 .cs  和程式集載入 .dll

提供支援路徑遞迴載入和指定檔案載入方式,並且提供了字尾篩選和附加dll載入。

using Microsoft.CSharp;
using System;
using System.CodeDom.Compiler;
using System.Collections.Concurrent;
using System.Collections.Generic;
using System.IO;
using System.Linq;
using System.Reflection;
using System.Text;

/**
 * 
 * @author 失足程式設計師
 * @Blog http://www.cnblogs.com/ty408/
 * @mail [email protected]
 * @phone 13882122019
 * 
 */
namespace Sz.Network.LoadScriptPool
{

    /// <summary>
    /// 載入指令碼檔案
    /// </summary>
    public class LoadScriptManager
    {

        private static LoadScriptManager instance = new LoadScriptManager();
        public static LoadScriptManager GetInstance { get { return instance; } }

        HashSet<String> ddlNames = new HashSet<string>();

        LoadScriptManager()
        {
            var asss = AppDomain.CurrentDomain.GetAssemblies();
            foreach (var item in asss)
            {
                foreach (var item222 in item.GetModules(false))
                {
                    ddlNames.Add(item222.FullyQualifiedName);
                }
            }
        }

        #region 返回查詢的指令碼例項 public IEnumerable<T> GetInstances<T>()
        /// <summary>
        /// 返回查詢的指令碼例項
        /// </summary>
        /// <typeparam name="T">型別</typeparam>
        /// <returns></returns>
        public IEnumerable<T> GetInstances<T>()
        {
            //使用列舉迭代器,避免了再一次建立物件
            string typeName = typeof(T).Name;
            if (Instances.ContainsKey(typeName))
            {
                foreach (var item in Instances[typeName])
                {
                    if (item.Value is T)
                    {
                        yield return (T)item.Value;
                    }
                }
            }
        }

        ConcurrentDictionary<string, ConcurrentDictionary<string, object>> Instances = new ConcurrentDictionary<string, ConcurrentDictionary<string, object>>();

        #endregion

        #region 根據指定的檔案動態編譯獲取例項 public void LoadCSharpFile(string[] paths, List<String> extensionNames, params string[] dllName)
        /// <summary>
        /// 根據指定的檔案動態編譯獲取例項
        /// <para>如果需要加入除錯資訊,加入程式碼 System.Diagnostics.Debugger.Break();</para>
        /// <para>如果傳入的是目錄。預設只會載入目錄中字尾“.cs”檔案</para>
        /// </summary>
        /// <param name="paths">
        /// 可以是目錄也可以是檔案路徑
        /// </param>
        /// <param name="dllName">載入的附加DLL檔案的路徑,絕對路徑</param>
        public void LoadCSharpFile(string[] paths, params string[] dllName)
        {
            LoadCSharpFile(paths, null, dllName);
        }

        List<String> csExtensionNames = new List<String>() { ".cs" };

        /// <summary>
        /// 根據指定的檔案動態編譯獲取例項
        /// <para>如果需要加入除錯資訊,加入程式碼 System.Diagnostics.Debugger.Break();</para>
        /// <para>如果傳入的是目錄。預設只會載入目錄中字尾“.cs”檔案</para>
        /// </summary>
        /// <param name="paths">
        /// 可以是目錄也可以是檔案路徑
        /// </param>
        /// <param name="extensionNames">需要載入目錄中的檔案字尾</param>
        /// <param name="dllName">載入的附加DLL檔案的路徑,絕對路徑</param>
        public void LoadCSharpFile(string[] paths, List<String> extensionNames, params string[] dllName)
        {
            GC.Collect();
            if (extensionNames == null)
            {
                extensionNames = csExtensionNames;
            }
            foreach (var item in dllName)
            {
                ddlNames.Add(item);
            }
            foreach (var item in ddlNames)
            {
                Console.WriteLine("載入依賴程式集:" + item);
            }
            List<String> fileNames = new List<String>();
            ActionPath(paths, extensionNames, ref fileNames);
            string[] tempDllNames = ddlNames.ToArray();
            foreach (var path in fileNames)
            {
                CSharpCodeProvider provider = new CSharpCodeProvider();
                CompilerParameters parameter = new CompilerParameters();
                //不輸出編譯檔案
                parameter.GenerateExecutable = false;
                //生成除錯資訊
                parameter.IncludeDebugInformation = true;
                //需要除錯必須輸出到記憶體
                parameter.GenerateInMemory = true;
                //新增需要的程式集
                parameter.ReferencedAssemblies.AddRange(tempDllNames);
                //編譯檔案
                Console.WriteLine("動態載入檔案:" + path);
                CompilerResults result = provider.CompileAssemblyFromFile(parameter, path);//根據制定的檔案載入指令碼
                if (result.Errors.HasErrors)
                {
                    var item = result.Errors.GetEnumerator();
                    while (item.MoveNext())
                    {
                        Console.WriteLine("動態載入檔案出錯了!" + item.Current.ToString());
                    }
                }
                else
                {
                    ActionAssembly(result.CompiledAssembly);
                }
            }
        }
        #endregion

        #region 根據指定的檔案動態編譯獲取例項 public void LoadDll(string[] paths)

        List<String> dllExtensionNames = new List<String>() { ".dll", ".DLL" };

        /// <summary>
        /// 根據指定的檔案動態編譯獲取例項
        /// <para>如果需要加入除錯資訊,加入程式碼 System.Diagnostics.Debugger.Break();</para>
        /// </summary>
        /// <param name="paths">
        /// 可以是目錄也可以是檔案路徑
        /// <para>如果傳入的是目錄。只會載入目錄中字尾“.dll”,“.DLL”檔案</para>
        /// </param>
        public void LoadDll(string[] paths)
        {
            GC.Collect();
            List<String> fileNames = new List<String>();
            ActionPath(paths, dllExtensionNames, ref fileNames);
            byte[] bFile = null;
            foreach (var path in fileNames)
            {
                try
                {
                    Console.WriteLine("動態載入檔案:" + path);
                    using (FileStream fs = new FileStream(path, FileMode.Open, FileAccess.Read))
                    {
                        using (BinaryReader br = new BinaryReader(fs))
                        {
                            bFile = br.ReadBytes((int)fs.Length);
                            ActionAssembly(Assembly.Load(bFile));
                        }
                    }
                }
                catch (Exception ex)
                {
                    Console.WriteLine("動態載入檔案:" + ex);
                }
            }
        }
        #endregion

        #region 處理加載出來的例項 void ActionAssembly(Assembly assembly)
        /// <summary>
        /// 處理加載出來的例項
        /// </summary>
        /// <param name="assembly"></param>
        void ActionAssembly(Assembly assembly)
        {
            ConcurrentDictionary<string, ConcurrentDictionary<string, object>> tempInstances = new ConcurrentDictionary<string, ConcurrentDictionary<string, object>>();
            //獲取載入的所有物件模型
            Type[] instances = assembly.GetExportedTypes();
            foreach (var itemType in instances)
            {
                //獲取單個模型的所有繼承關係和介面關係
                Type[] interfaces = itemType.GetInterfaces();
                //生成例項
                object obj = Activator.CreateInstance(itemType);
                foreach (var iteminterface in interfaces)
                {
                    //加入物件集合
                    if (!Instances.ContainsKey(iteminterface.Name))
                    {
                        tempInstances[iteminterface.Name] = new ConcurrentDictionary<string, object>();
                    }
                    tempInstances[iteminterface.Name][itemType.Name] = obj;
                }
            }
            Instances = tempInstances;
        } 
        #endregion

        #region 處理傳入的路徑 void ActionPath(string[] paths, List<String> extensionNames, ref List<String> fileNames)
        /// <summary>
        /// 處理傳入的路徑,
        /// </summary>
        /// <param name="paths"></param>
        /// <param name="extensionNames"></param>
        /// <param name="fileNames"></param>
        void ActionPath(string[] paths, List<String> extensionNames, ref List<String> fileNames)
        {
            foreach (var path in paths)
            {
                if (System.IO.Path.HasExtension(path))
                {
                    if (System.IO.File.Exists(path))
                    {
                        fileNames.Add(path);
                    }
                    else
                    {
                        Console.WriteLine("動態載入 無法找到檔案:" + path);
                    }
                }
                else
                {
                    GetFiles(path, extensionNames, ref fileNames);
                }
            }
        }
        #endregion

        #region 根據指定資料夾獲取指定路徑裡面全部檔案 void GetFiles(string sourceDirectory, List<String> extensionNames, ref  List<String> fileNames)
        /// <summary>
        /// 根據指定資料夾獲取指定路徑裡面全部檔案
        /// </summary>
        /// <param name="sourceDirectory">目錄</param>
        /// <param name="extensionNames">需要獲取的副檔名</param>
        /// <param name="fileNames">返回檔名</param>
        void GetFiles(string sourceDirectory, List<String> extensionNames, ref  List<String> fileNames)
        {
            if (!Directory.Exists(sourceDirectory))
            {
                return;
            }
            {
                //獲取所有檔名稱
                string[] fileName = Directory.GetFiles(sourceDirectory);
                foreach (string path in fileName)
                {
                    if (System.IO.File.Exists(path))
                    {
                        string extName = System.IO.Path.GetExtension(path);
                        if (extensionNames.Contains(extName))
                        {
                            fileNames.Add(path);
                        }
                        else
                        {
                            Console.WriteLine("無法識別檔案:" + path);
                        }
                    }
                    else
                    {
                        Console.WriteLine("動態載入 無法找到檔案:" + path);
                    }
                }
            }
            //拷貝子目錄       
            //獲取所有子目錄名稱
            string[] directionName = Directory.GetDirectories(sourceDirectory);
            foreach (string directionPath in directionName)
            {
                //遞迴下去
                GetFiles(directionPath, extensionNames, ref fileNames);
            }
        }
        #endregion
    }
}
View Code