65.9K
CodeProject 正在变化。 阅读更多。
Home

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

starIconstarIconstarIconstarIcon
emptyStarIcon
starIcon

4.50/5 (2投票s)

2017 年 7 月 26 日

CPOL

4分钟阅读

viewsIcon

6523

JointCode.Shuttle 是一个快速、灵活且易于使用的面向服务的框架,用于跨 AppDomain 通信。它是运行时库提供的 MarshalByrefObject 的替代品。

下载 JointCode.Shuttle 和示例

引言

本文简要介绍了一种新技术,该技术可以在应用程序域之间进行封送处理时提高功能和性能。

背景

所有 .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();
        }
    }
}

尽管简单,但使用此方法存在一些限制。

  1. AppDomain 是一个单点访问基础设施,也就是说,子 AppDomain 只能由创建它的父(或宿主)AppDomain 访问,其他任何 AppDomain 都无法访问,即使是宿主的父 AppDomain 或由同一宿主创建的兄弟 AppDomain 也无法访问。
  2. 缺乏灵活性,因为服务必须继承自 MarshalByrefObject,这限制了灵活性。
  3. 性能问题,测试表明,使用这种方式进行跨 AppDomain 方法调用比在同一 AppDomain 内进行普通方法调用慢数百到数千倍。
  4. 双向通信,无法通过这种方式实现双向通信。

此外,还有 Remoting、WCF、甚至消息队列以及其他 IPC 机制也可以用于实现跨 AppDomain 通信,并且这些方法或多或少消除了上述限制。然而,我们可以想象,性能损失肯定比 MarshalByrefObject 更严重,并且学习成本更高,实现将更加复杂。

所以,似乎没有好的解决方案。是这样吗?

解决方案

解决方案很简单,换个方式!这就是我创建 JointCode.Shuttle 的原因。

JointCode.Shuttle 是一个快速、灵活且易于使用的面向服务的框架,用于跨 AppDomain 通信。它旨在取代运行时库提供的 MarshalByrefObject

JointCode.Shuttle 提供与 MarshalByrefObject 相同的跨 AppDomain 通信功能,并具有以下特点:

  1. 面向服务。
  2. 从一个 AppDomain 访问任何 AppDomainMarshalByrefObject 只允许从父 AppDomain 访问子 AppDomain)。
  3. 更好的性能:比 MarshalByrefObject 快 60 到 70 倍。
  4. 服务可管理:在运行时动态注册/注销服务,无需重新启动应用程序,甚至无需重新启动 AppDomain
  5. 强类型,易于使用(而 MarshalByrefObject 的方式依赖于魔术字符串来查找服务类型)。
  6. 内置 IoC 功能,用于自动管理服务依赖。
  7. 支持延迟类型/程序集加载。
  8. 远程服务的生命周期可以通过租约或按需管理(MarshalByrefObject 的方式不提供远程服务生命周期管理)。
  9. 简单快捷,易于上手。
  10. 支持 .net 2.0。

如何使用

JointCode.Shuttle 分发包中,包含两个文件:JointCode.Shuttle.dllJointCode.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.SimpleTest

未来

JointCode.Shuttle 仍然是一个年轻的项目,它缺少一些功能,例如服务注册/注销通知、跨 AppDomain 事件等。作者将继续改进现有功能,并在未来引入更多新功能,但目前存在一些限制,包括:

  1. 仅支持 32 位应用程序(x86 目标平台)。
  2. 仅支持 Windows(仅支持 .net framework,目前不支持 mono)。
  3. 服务接口中不支持事件。
  4. 不支持跨 AppDomain 事件。
  5. 未经彻底测试。
© . All rights reserved.