JointCode.Shuttle,一个快速、灵活且易于使用的面向服务的框架,用于跨 AppDomain 通信






4.50/5 (2投票s)
JointCode.Shuttle 是一个快速、灵活且易于使用的面向服务的框架,用于跨 AppDomain 通信。它是运行时库提供的 MarshalByrefObject 的替代品。
引言
本文简要介绍了一种新技术,该技术可以在应用程序域之间进行封送处理时提高功能和性能。
背景
所有 .net/mono 代码在底层都在 AppDomain
边界内运行,但开发这些应用程序时 AppDomain
并不是一项常用技术,这是因为通常我们的代码运行在一个 AppDomain
(默认 AppDomain
)中,我们不会自己创建它,而是由运行时系统为我们创建。
但是,有时您确实需要创建第二个 AppDomain
并在其中运行代码。例如,当您需要在运行时加载和卸载程序集而不停止应用程序时。
在这些情况下,您需要进行跨 AppDomain
调用。那么,问题是什么?嗯,问题不在于您进行调用,而在于您如何进行这些调用。
通常,大多数人使用运行时提供的基于 MarshalByrefObject
子类化的机制来实现跨 AppDomain
通信。这是最简单、最方便的方法,而且一切都会很好,只要要跨 AppDomain
操作的服务继承自 MarshalByrefObject
,如下所示:
namespace JoitCode.Shuttle.SimpleSample
{
public class MyService : MarshalByRefObject
{
public void Do() { }
}
class Program
{
static void Main(string[] args)
{
var serviceDomain = AppDomain.CreateDomain("ServiceDomain", null, null);
var myService = (MyService)serviceDomain.CreateInstanceAndUnwrap
(typeof(MyService).Assembly.FullName,
"JoitCode.Shuttle.SimpleSample.MyService");
myService.Do();
Console.Read();
}
}
}
尽管简单,但使用此方法存在一些限制。
- AppDomain 是一个单点访问基础设施,也就是说,子
AppDomain
只能由创建它的父(或宿主)AppDomain
访问,其他任何 AppDomain 都无法访问,即使是宿主的父AppDomain
或由同一宿主创建的兄弟AppDomain
也无法访问。 - 缺乏灵活性,因为服务必须继承自
MarshalByrefObject
,这限制了灵活性。 - 性能问题,测试表明,使用这种方式进行跨
AppDomain
方法调用比在同一AppDomain
内进行普通方法调用慢数百到数千倍。 - 双向通信,无法通过这种方式实现双向通信。
此外,还有 Remoting、WCF、甚至消息队列以及其他 IPC 机制也可以用于实现跨 AppDomain
通信,并且这些方法或多或少消除了上述限制。然而,我们可以想象,性能损失肯定比 MarshalByrefObject
更严重,并且学习成本更高,实现将更加复杂。
所以,似乎没有好的解决方案。是这样吗?
解决方案
解决方案很简单,换个方式!这就是我创建 JointCode.Shuttle 的原因。
JointCode.Shuttle 是一个快速、灵活且易于使用的面向服务的框架,用于跨 AppDomain
通信。它旨在取代运行时库提供的 MarshalByrefObject
。
JointCode.Shuttle 提供与 MarshalByrefObject
相同的跨 AppDomain
通信功能,并具有以下特点:
- 面向服务。
- 从一个
AppDomain
访问任何AppDomain
(MarshalByrefObject
只允许从父AppDomain
访问子AppDomain
)。 - 更好的性能:比
MarshalByrefObject
快 60 到 70 倍。 - 服务可管理:在运行时动态注册/注销服务,无需重新启动应用程序,甚至无需重新启动
AppDomain
。 - 强类型,易于使用(而
MarshalByrefObject
的方式依赖于魔术字符串来查找服务类型)。 - 内置 IoC 功能,用于自动管理服务依赖。
- 支持延迟类型/程序集加载。
- 远程服务的生命周期可以通过租约或按需管理(
MarshalByrefObject
的方式不提供远程服务生命周期管理)。 - 简单快捷,易于上手。
- 支持 .net 2.0。
如何使用
在 JointCode.Shuttle 分发包中,包含两个文件:JointCode.Shuttle.dll 和 JointCode.Shuttle.Library.dll,其中 JointCode.Shuttle.dll 是用托管语言编写的库,而 JointCode.Shuttle.Library.dll 是一个用非托管语言编写的组件,前者依赖于后者。
准备
要使用 JointCode.Shuttle,我们首先需要在项目中引用 JointCode.Shuttle.dll,并将 JointCode.Shuttle.Library.dll 复制到编译 JointCode.Shuttle.dll 的文件夹中(例如,项目编译后,如果 JointCode.Shuttle.dll 被复制到 c:/projects/sampleproject 文件夹,您需要手动将 JointCode.Shuttle.Library.dll 复制到该文件夹)。
使用代码
由于 JointCode.Shuttle 是面向接口的,因此我们首先需要创建一个服务接口(也称为服务契约),并为其应用 ServiceInterface
属性。
[ServiceInterface]
public interface ISimpleService
{
string GetOutput(string input);
}
然后,我们创建一个实现契约并应用 ServiceClass
属性的服务类。
[ServiceClass(typeof(ISimpleService), Lifetime = LifetimeEnum.Transient)]
public class SimpleService : ISimpleService
{
public string GetOutput(string input)
{
return string.Format
("SimpleService.GetOutput says: now, we are running in AppDomain: {0}, and the input passed from the caller is: {1}",
AppDomain.CurrentDomain.FriendlyName, input);
}
}
因为我们要进行跨 AppDomain
调用,所以我们需要编写一个用于启动/停止远程服务的类,并让它继承自 MarshalByRefObject
。
public class ServiceProvider : MarshalByRefObject
{
// We need a field to keep the _shuttleDomain alive, because if it is garbage collected,
// we'll lose all communications with other AppDomains.
ShuttleDomain _shuttleDomain;
public void RegisterServices()
{
// A Guid is needed when registering service group
var guid = Guid.NewGuid();
_shuttleDomain.RegisterServiceGroup(ref guid,
new ServiceTypePair(typeof(ISimpleService), typeof(SimpleService)));
}
public void CreateShuttleDomain()
{
// Create a ShuttleDomain object
_shuttleDomain = ShuttleDomainHelper.Create("domain1", "domain1");
}
public void DisposeShuttleDomain()
{
_shuttleDomain.Dispose();
}
}
现在,我们准备好使用 JointCode.Shuttle 了。
namespace JoitCode.Shuttle.SimpleSample
{
public static class ShuttleDomainHelper
{
public static ShuttleDomain Create(string assemblySymbol, string assemblyName)
{
return Create(assemblySymbol, assemblyName, null);
}
public static ShuttleDomain Create(string assemblySymbol, string assemblyName, ServiceContainer svContainer)
{
var dynAsmOptions = new DynamicAssemblyOptions
{
AccessMode = AssemblyBuilderAccess.Run,
AssemblyName = new AssemblyName(assemblyName)
};
var options = new ShuttleDomainOptions
{
DynamicAssemblySymbol = assemblySymbol,
DynamicAssemblyOptions = dynAsmOptions,
DefaultLeaseTime = 10.Seconds(),
PollingInterval = 5.Seconds()
};
try
{
return ShuttleDomain.Create(ref options, svContainer);
}
catch (Exception e)
{
if (e.InnerException != null)
Console.WriteLine(e.InnerException.Message);
else
Console.WriteLine(e.Message);
return null;
}
}
}
class Program
{
static void Main(string[] args)
{
// To be able to make inter-AppDomain communication with JointCode.Shuttle, firstly we must
// initialize the ShuttleDomain.
// It doesn't matter whether the initialization operation is done in default AppDomain or
// any other AppDomains, but it must be done before any ShuttleDomain instance is created.
ShuttleDomain.Initialize();
// Creating a child AppDomain in default AppDomain.
var serviceEnd1Domain = AppDomain.CreateDomain("ServiceEndDomain1", null, null);
// Creating a ServiceProvider instance for operating that child AppDomain.
var serviceProvider = (ServiceProvider)serviceEnd1Domain.CreateInstanceAndUnwrap
(typeof(Program).Assembly.FullName, "JoitCode.Shuttle.SimpleSample.ServiceProvider");
// Creating a ShuttleDomain instance in the child AppDomain.
serviceProvider.CreateShuttleDomain();
// Registering ISimpleService service in child AppDomain.
serviceProvider.RegisterServices();
// Creating a ShuttleDomain instance in default AppDomain.
// Actually, we needs to create one and only one ShuttleDomain instance in every AppDomains.
// The ShuttleDomain instances communicates with each other across AppDomains.
var str = Guid.NewGuid().ToString();
var shuttleDomain = ShuttleDomainHelper.Create(str, str);
// Get the ISimpleService service in default AppDomain, which is registered by the child AppDomain.
// The lifetime of service is default to 1 minute, every call to the service method
// extends that time for 30 seconds.
ISimpleService service;
if (shuttleDomain.TryGetService(out service))
{
try
{
Console.WriteLine("Currently, we are running in AppDomain {0} before calling the remote service method...",
AppDomain.CurrentDomain.FriendlyName);
Console.WriteLine();
// Call the service method of ISimpleService service.
var output = service.GetOutput("China");
Console.WriteLine(output);
Console.WriteLine();
Console.WriteLine("Tests completed...");
}
catch
{
Console.WriteLine();
Console.WriteLine("Failed to invoke the remote service method...");
}
}
else
{
Console.WriteLine();
Console.WriteLine("Failed to create remote service instance...");
}
// Indicate the child AppDomain to release the ISimpleService service immediately, instead of waiting for its lifetime to end.
// This is optional, because even if we don't do this explicitly, the ISimpleService service will still get released in the
// child AppDomain automatically when its lifetime ends.
// And, if the ISimpleService derives from IDisposable, the Dispose method will also get called at that time.
shuttleDomain.ReleaseService(service);
// Releasing the ShuttleDomain instance in the child AppDomain, this will unregister all services registered by that
// instance, and shut down all communications between that child AppDomain and all other AppDomains.
serviceProvider.DisposeShuttleDomain();
Console.Read();
}
}
}
这是结果的屏幕截图。
未来
JointCode.Shuttle 仍然是一个年轻的项目,它缺少一些功能,例如服务注册/注销通知、跨 AppDomain
事件等。作者将继续改进现有功能,并在未来引入更多新功能,但目前存在一些限制,包括:
- 仅支持 32 位应用程序(x86 目标平台)。
- 仅支持 Windows(仅支持 .net framework,目前不支持 mono)。
- 服务接口中不支持事件。
- 不支持跨
AppDomain
事件。 - 未经彻底测试。