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

通用 WCF 主机

starIconstarIconstarIconstarIcon
emptyStarIcon
starIcon

4.90/5 (13投票s)

2008 年 3 月 13 日

CPOL

5分钟阅读

viewsIcon

92151

downloadIcon

1153

通用控制台托管器,不会锁定服务程序集,使用 VS2005 加速 WCF 服务开发

generic_wcf_host/ConsoleHoster.gif

背景

当您需要开发 WCF 服务时,您需要将其托管在某个地方。到目前为止,您有三种选择:IIS 6.0(仅限 HTTP)、IIS7/WAS(Windows 2008/Vista)或自托管(桌面应用程序、Windows 服务等)。

快速浏览一下其他开发网站,会发现开发者们大多最终使用桌面应用程序编写自己的 WCF 托管器。主要原因是习惯使然:成本低、简单、有趣。创建一个新的控制台应用程序,添加对 WCF 项目的引用,五行代码即可完成。

在其他一些网站上,您可以看到人们编写 Windows 服务来充当托管器。但这种方法存在一些明显的缺点:错误会写入事件查看器,程序集会被独占锁定(因此您无法重新编译该程序集),并且还需要额外的步骤来停止/重新编译/重新启动托管器(mav 写了一篇关于这个的技巧)。

为了帮助解决这些场景,我编写了一个通用的控制台托管器,它会在这些程序集重新编译时接收通知,卸载它们的 AppDomain 并启动一个新的。为了避免无聊、几乎静态的黑屏,我还显示了一些关于通过此托管器接收和发送的消息以及可能发生的异常的信息。

如果您使用的是 Visual Studio 2008,则有两个实用程序可以执行相同的操作:《WCF 服务主机》和《WCF 测试客户端》。它们可以在您的 %ProgramFiles%\Microsoft Visual Studio 9.0\Common7\IDE 目录中找到。

Using the Code

监视文件系统

我们开始配置 DelayedFileSystemWatcher 对象,并查找已存在于配置目录中的所有程序集。

static Dictionary<string, AppDomain> appDomains = new Dictionary<string, AppDomain>();

static void Main(string[] args)
{
    // can't find settings at *.exe.config file? look at current directory
    const string pattern = "*.dll";
    string dropPath = Path.GetFullPath(
        args.Length == 0 ?
            ConfigurationManager.AppSettings["DropPath"] ?? 
                Environment.CurrentDirectory : args[0]);

    Console.Title = dropPath + Path.DirectorySeparatorChar + pattern;
    if (!Directory.Exists(dropPath))
        throw new DirectoryNotFoundException(dropPath);

    // sets up file system monitoring
    DelayedFileSystemWatcher dfsw = new DelayedFileSystemWatcher(dropPath, pattern);
    dfsw.Created += new FileSystemEventHandler(dfsw_CreatedOrChanged);
    dfsw.Changed += new FileSystemEventHandler(dfsw_CreatedOrChanged);
    dfsw.Deleted += new FileSystemEventHandler(dfsw_Deleted);

    // before start monitoring disk, load already existing assemblies
    foreach(string assemblyFile in Directory.GetFiles(dropPath, pattern))
    {
        Create(Path.GetFullPath(assemblyFile));
    }

    dfsw.EnableRaisingEvents = true;
    Console.ReadLine(); // stay away from ENTER key
    Console.ResetColor();
}

请注意,我使用的是由Adrian Hamza编写的 DelayedFileSystemWatcher。他的类封装了常规的 FileSystemWatcher,并添加了一个时间池。如果一个时间间隔(在本例中为一秒)内触发多个事件,它会将它们排队,并删除重复的事件。

我订阅的事件让我能够维护 AppDomains 列表。如果程序集被 Deleted,我需要卸载其 AppDomain;如果被 CreatedChanged,我需要卸载并重新创建它,因为我无法从一个 AppDomain 中卸载单个程序集。基本上,我将创建的 AppDomain 保存在一个通用的 Dictionary<string, AppDomain> 中,使用程序集的完整路径作为键,并捕获所有可能发生的异常,将它们写入控制台。

由于重点不在这里,我将跳过 AppDomain 创建的细节。您可以在本文的附件中找到完整且可运行的代码。

创建新的 AppDomain

在此步骤中,我必须创建一个继承自 MarshalByRefObject 类的类。我需要在外部 AppDomain 中实例化此类,并在其中启动 WCF 服务托管器。但有一些小技巧需要注意:

  • 在创建 AppDomain 之前,您需要为您的服务程序集提供正确的配置文件。
  • 您需要启用程序集的阴影复制(使用 ShadowCopyFiles 属性),否则您的 DLL 文件将被锁定。简单来说,框架将其复制到另一个位置,保留原始文件以便更改。请注意,这是一个 string 属性;
  • 我曾尝试在创建 AppDomains 之前使用 Assembly.ReflectionOnlyLoadFrom(assemblyFile) 来检查服务程序集中的 WCF 服务,但该方法也会锁定程序集文件。这种技术在 Kader Yildirim 的一篇法语文章中有详细介绍
  • 当您使用代理类的完整名称调用 CreateInstanceAndUnwrap 时,代理实例将在新的 AppDomain 中创建。这就是魔力所在。
class RemoteProxy : MarshalByRefObject
{
    static public AppDomain Start(string assemblyFile)
    {
        AppDomain domain = null;
        try
        {
            AppDomainSetup info = new AppDomainSetup();
            info.ShadowCopyFiles = "true";
            info.ConfigurationFile = assemblyFile + ".config";

            AppDomain appDomain =
                AppDomain.CreateDomain(
                    assemblyFile, null, info);

            RemoteProxy proxy = (RemoteProxy)appDomain
                .CreateInstanceAndUnwrap(
                    Assembly.GetExecutingAssembly().FullName,
                    typeof(RemoteProxy).FullName);
            if (!proxy.LoadServices(assemblyFile))
                AppDomain.Unload(appDomain);
            else
                domain = appDomain;
        }
        catch (Exception ex)
        {
            Console.ForegroundColor = ConsoleColor.Red;
            Console.WriteLine(ex.Message);
        }
        return domain;
    }
    
    // ...
}

为了创建我们的 ServiceHost 并开始监听我们的服务,我们需要找到具有 [ServiceContract] 属性的类。该属性可以定义在我们的类中,也可以定义在一个继承的接口上。这种递归代码也包含在随附的 zip 文件中。

托管 WCF 服务

现在,让我们加载服务程序集并查找 WCF 服务实现。您需要创建一个 AssemblyName 来告知程序集的代码库、其他依赖项可以找到的路径。一旦找到这些服务实现,我们最终将创建我们的 ServiceHost

bool hasServices = false;

public bool LoadServices(string assemblyFile)
{
    try
    {
        AssemblyName assemblyRef = new AssemblyName();
        assemblyRef.CodeBase = assemblyFile;
        Assembly assembly = Assembly.Load(assemblyRef);

        Type[] serviceTypes = LocateServices(assembly.GetTypes());
        foreach (Type serviceType in serviceTypes)
        {
            try
            {
                Create(serviceType);
            }
            catch (Exception ex)
            {
                Console.ForegroundColor = ConsoleColor.Red;
                Console.WriteLine(ex.Message);
            }
        }
    }
    catch(Exception ex)
    {
        Console.ForegroundColor = ConsoleColor.Red;
        Console.WriteLine(ex.Message);
    }
    return hasServices;
}

到目前为止,我们位于一个新的 AppDomain 中,指向我们的服务程序集配置文件,因此我们拥有关于端点的所有必要信息。对于每个加载的端点,我们将添加一个服务器端点行为来检查接收到的消息,用于调试目的。

private void Create(Type serviceType)
{
    Console.ForegroundColor = ConsoleColor.White;
    Console.WriteLine("Starting {0}", serviceType.FullName);
    try
    {
        ServiceHost host = new ServiceHost(serviceType, new Uri[0]);
        foreach (ServiceEndpoint endpoint in host.Description.Endpoints)
        {
            Console.ForegroundColor = ConsoleColor.White;
            Console.WriteLine("   {0}", endpoint.Address);
            endpoint.Behaviors.Add(new MonitorBehavior());
        }
        host.Open();
        hasServices = true;
    }
    catch (Exception ex)
    {
        Console.ForegroundColor = ConsoleColor.Red;
        Console.WriteLine(ex.Message);
    }
}

“展示你隐藏的信息”

最后,让我们创建一个行为来收集有关入站/出站消息的信息。从 MonitorBehavior 类开始,它实现了 IEndpointBehavior 接口。在这种情况下,所有方法的实现都是空的,除了 ApplyDispatchBehavior,它负责附加我们的 MonitorDispatcher,如下所示:

class MonitorBehavior : IEndpointBehavior
{
    // ... empty methods removed ... \\

    public void ApplyDispatchBehavior(
        ServiceEndpoint endpoint,
        EndpointDispatcher endpointDispatcher)
    {
        endpointDispatcher.DispatchRuntime.MessageInspectors
           .Add(new MonitorDispatcher());
    }
    
    class MonitorDispatcher : IDispatchMessageInspector
    {
        public object AfterReceiveRequest(
            ref Message request, 
            IClientChannel channel, 
            InstanceContext instanceContext)
        {
            Console.ForegroundColor = ConsoleColor.Green;
            Console.WriteLine(
                "{0:HH:mm:ss.ffff}\t{1}\n\t\t{2} ({3} bytes)\n\t\t{4}",
                DateTime.Now, request.Headers.MessageId, 
                request.Headers.Action, request.ToString().Length, 
                request.Headers.To);
            return null;
        }

        public void BeforeSendReply(
            ref Message reply,
            object correlationState)
        {
            Console.ForegroundColor = ConsoleColor.Green;
            Console.WriteLine(
                "{0:HH:mm:ss.ffff}\t{1}\n\t\t{2} ({3} bytes)",
                DateTime.Now, reply.Headers.RelatesTo,
                reply.Headers.Action, reply.ToString().Length);
        }
    }
}

上面的代码会在控制台转储消息 ID、时间戳、长度以及入站和出站消息请求的操作。这对于查找返回过多信息(例如,一个大集合)的服务,或者仅仅查看消息 XML 内容(调用 ToString())都很有用。

运行代码

您会看到一个类似这样的屏幕:

generic_wcf_host/ConsoleHoster2.gif

文章附件包含两个项目:

  • ConsoleHoster:托管服务器应用程序。您应该先启动它。您可以在 app.config 文件中更改要监视的目录,该文件显示在窗口标题栏中。按 ENTER 键将退出此应用程序。
  • ServiceImplementation:一个示例实现(服务和客户端)。客户端部分仅调用服务,该服务将在服务器部分进行托管。运行它,按任意键再次调用服务,或按 ESC 键退出。注意当您重新编译此应用程序时,服务器托管器上触发的事件。

关注点

虽然我没有使用 AppDomain 的丰富经验,但此实现在我当前的环境中运行得相当顺利,并且我成功地部署了一个旨在容纳服务托管器的 Windows 服务。

为了进一步阅读,我想推荐 Suzanne Cook 的博客和 Bart De Smet 的博客,我可以在那里找到关于 AppDomainsShadowCopy 的大量信息。

历史

  • 2008 年 3 月 16 日:1.0:第一个版本
© . All rights reserved.