目前在 .NET 环境下,可选的 "插件模型" 包括:
1. 程序集方式
- System.Addin : .NET 3.5 BCL 提供的一个插件扩展模型,功能十分强大,不过整体有些偏 "重",过于复杂。
- System.Reflection : 使用反射过于繁琐了些。
- AOP : 基本和反射类似,不过可以通过配置文件来简化操作。
2. 脚本方式
- System.CodeDom : 这个不错,不过要处理的细节也不少,诸如编译过程,引用及参数设置等等。
- IronPython、Ruby.NET : 很多网络游戏服务器,都采取动态脚本来编写 "剧情",服务器本身只是一个执行环境。只是选择 Python、Ruby 需要不同的语言整合,有点麻烦。
- CS-Script : 这个东西基本上是对 CodeDom 的封装,它为我们完成了所有的细节处理。
经过对比,CS-Script 更符合我的需求。这样一来,我们只需要将策略写成文本代码文件,放到特定区域,然后由 "Shell" 载入执行即可。就算修改,也只需一个记事本就能完成,更何况使用的还是我们熟悉的 C# 语言。(记事本 + 命令行编译器 + 命令行调试器 ~~~~ 不要学我们这的某个同事,要在服务器上装一整套 VS2005。 ~~~~)
看一个例子。
Server.exe
using System;
using System.Collections.Generic;
using System.Linq;
using System.Text;
using CSScriptLibrary;
namespace Server
{
// 服务器上下文环境
public class MyContent
{
public int I { get; set; }
}
// 插件接口
public interface IAddin
{
void Execute(MyContent content);
}
class Program
{
static void Main(string[] args)
{
// 载入策略脚本
var assembly = CSScript.Load("TestAddin.cs");
// 查找实现了插件接口的类型
var q = (from type in assembly.GetTypes()
where type.GetInterface("Server.IAddin", true) != null select type).SingleOrDefault();
// 创建环境上下文
var content = new MyContent { I = 12345 };
// 创建插件对象 (当然,我们可以缓存该对象,没必要每次执行都创建一个实例)
var addin = new AsmHelper(assembly).CreateObject(q.Name) as IAddin;
// 调用插件方法
addin.Execute(content);
}
}
}
using System.Collections.Generic;
using System.Linq;
using System.Text;
using CSScriptLibrary;
namespace Server
{
// 服务器上下文环境
public class MyContent
{
public int I { get; set; }
}
// 插件接口
public interface IAddin
{
void Execute(MyContent content);
}
class Program
{
static void Main(string[] args)
{
// 载入策略脚本
var assembly = CSScript.Load("TestAddin.cs");
// 查找实现了插件接口的类型
var q = (from type in assembly.GetTypes()
where type.GetInterface("Server.IAddin", true) != null select type).SingleOrDefault();
// 创建环境上下文
var content = new MyContent { I = 12345 };
// 创建插件对象 (当然,我们可以缓存该对象,没必要每次执行都创建一个实例)
var addin = new AsmHelper(assembly).CreateObject(q.Name) as IAddin;
// 调用插件方法
addin.Execute(content);
}
}
}
TestAddin.cs
//css_ref Server.exe;
using System;
using Learn.CUI;
public class TestAddin : IAddin
{
public void Execute(MyContent content)
{
Console.WriteLine(content.I);
}
}
using System;
using Learn.CUI;
public class TestAddin : IAddin
{
public void Execute(MyContent content)
{
Console.WriteLine(content.I);
}
}
当然,我们还可以在另一个应用程序域中执行插件,这样就可以 Unload,从而在不关闭宿主的情况下修改插件脚本了。
Server.cs
using System;
using System.Collections.Generic;
using System.Linq;
using System.Text;
using CSScriptLibrary;
namespace Server
{
// 服务器上下文环境,注意 MarshalByRefObject。
public class MyContent : MarshalByRefObject
{
public int I { get; set; }
}
// 插件接口
public interface IAddin
{
void Execute(MyContent content);
}
class Program
{
static void Main(string[] args)
{
var content = new MyContent { I = 12345 };
var asmFile = CSScript.Compile("TestAddin.cs");
using (var helper = new AsmHelper(asmFile, "AddinDomain", true))
{
// 注意设置依赖程序集搜索路径
helper.ProbingDirs = new[] { AppDomain.CurrentDomain.BaseDirectory };
// 创建代理对象
var addin = helper.CreateObject("TestAddin") as IAddin;
// 调用插件方法
addin.Execute(content);
}
}
}
}
using System.Collections.Generic;
using System.Linq;
using System.Text;
using CSScriptLibrary;
namespace Server
{
// 服务器上下文环境,注意 MarshalByRefObject。
public class MyContent : MarshalByRefObject
{
public int I { get; set; }
}
// 插件接口
public interface IAddin
{
void Execute(MyContent content);
}
class Program
{
static void Main(string[] args)
{
var content = new MyContent { I = 12345 };
var asmFile = CSScript.Compile("TestAddin.cs");
using (var helper = new AsmHelper(asmFile, "AddinDomain", true))
{
// 注意设置依赖程序集搜索路径
helper.ProbingDirs = new[] { AppDomain.CurrentDomain.BaseDirectory };
// 创建代理对象
var addin = helper.CreateObject("TestAddin") as IAddin;
// 调用插件方法
addin.Execute(content);
}
}
}
}
TestAddin.cs
//css_ref Server.exe;
using System;
using Server;
// 插件,注意 MarshalByRefObject。
public class TestAddin : MarshalByRefObject, IAddin
{
public void Test()
{
Console.WriteLine(AppDomain.CurrentDomain.FriendlyName);
}
public void Execute(MyContent content)
{
Console.WriteLine(content.I);
}
}
using System;
using Server;
// 插件,注意 MarshalByRefObject。
public class TestAddin : MarshalByRefObject, IAddin
{
public void Test()
{
Console.WriteLine(AppDomain.CurrentDomain.FriendlyName);
}
public void Execute(MyContent content)
{
Console.WriteLine(content.I);
}
}
由于要跨域传递,因此某些地方需要添加 MarshalByRefObject。另外,注意设置 helper.ProbingDirs,否则找不到插件依赖的程序集,抛出异常。雨痕偷懒没有把公用类型放到一个单独的程序集中,正式开发时,建议将其放到一个独立的类库项目中。
CS-Script 的功能很多,用它替代 CodeDom,可以省不少力气。更多细节,请参考其帮助文件。