在 Console 主机进程上记录 Indigo 消息






3.40/5 (22投票s)
Tiny Logger 是一个用于消息总线 (MessageBus) 上 Indigo 消息交换模式的轻量级日志记录解决方案。本文介绍如何构建一个 Indigo 端口扩展 (Port Extension) 来在控制台宿主进程中记录和发布消息。它对于评估 MSDN Indigo 示例或用于测试目的非常有用。
本文基于 Microsoft Windows 代号为“Longhorn”的 PDC 2003 之前的版本编写。此预览版距离 Beta 版本还有很长的路要走,我敢肯定在某些细节上它不会起作用。我会尽快发布更新。我已将日志记录类实现包含在此文档中,因此如果您没有 PDC 的位,只需在此处查看即可。
目录
引言
Indigo (MS Windows 下一代代码 - Longhorn 的一部分) 是面向服务架构 (SOA) 的一种新的消息驱动通信模型,它支持跨异构通信系统进行安全、可靠、事务性的消息传递。下图展示了其逻辑模型。
上述高度抽象的通信模型基于软件总线 (Software Bus),该总线允许通过服务连接器 (Service connector) 连接应用程序代码。服务层封装了业务逻辑模型中的连接性,这样服务的使用者就不需要知道物理业务逻辑位于何处以及如何实现。Indigo 代表了一种松散耦合的消息推送通信模型,该模型基于消息交换模式 (Message Exchange Patterns)。Indigo 消息通过软件总线以直接或转发的方式流向目标服务。
软件总线使用抽象为“消息”的 SoapMessage 信封来标准化服务之间的连接,其中:
- Headers 表示在软件总线上处理消息的通信模式(连接性、消息交换等)。
- Content 表示通过服务连接器传递给 CodeBehind 的业务数据。请注意,在特殊情况下,如 WS-Eventing,消息内容也可以是通信模式的一部分。
软件总线上的消息是一个有状态对象,在指定服务节点上具有预定义的生命周期和行为。根据 Headers 的类型,消息会携带或不携带 Content。此行为由服务管理器 (Service Managers) 控制,作为服务与其使用者之间的逻辑对话过程。
软件总线在 Indigo 模型中由
System.MessageBus
命名空间实现。
CodeBehind(业务逻辑)通过服务连接器连接到软件总线。该连接器封装了 Indigo 基础设施的三个主要部分。下图展示了它们在消息工作流中的位置。
Port 代表服务的进/出网关。消息通过发送或/和接收管道阶段以逻辑方式处理,以根据消息头完成特定任务。使用基类 PortExtension
,可以自定义端口管道中的消息工作流以满足其他目的,例如日志记录、自定义对话等。请注意,端口通过传输适配器 (Transport adapter) 连接到网络(软件总线),以处理特定格式(如 HTTP 或 TCP,当然也可以是自定义格式)的底层网络协议。
Channel 代表执行通信行为并与端口进行消息模式传输的层。服务连接器中有两种通道:
- Untyped channel(非类型化通道)代表较低级别的非类型化消息模式,用于接收、发送、反向和转发消息。这种类型的通道实现为
PortExtension
。 - Typed channel(类型化通道)是服务和端口之间强消息交换模式的最高层。Indigo 基础设施内置了两种类型化通道,如 Datagram(不保证消息传递)和 Dialog 通道,用于服务之间可靠和事务性的消息交换。
Service 代表 Indigo 基础设施的顶层,使用强类型方法调用设计模式封装了业务逻辑的通信模型。
Indigo 根据应用程序需求提供通信复杂性,由其服务管理器负责。管理器使用内置的 PortExtension
,如 DialogPortExtension
、ReplyPortExtension
等来填充端口管道。此“加载”过程可以根据应用程序需求进行管理式或编程式地完成。请注意,在端口打开之前,可以接受配置更改。Indigo 预定义了 ServiceEnvironment
(称为“main” - 请参阅 machine.config 文件)模板配置,可以在 app.config 文件中或在宿主进程文件中以编程方式进行调整。
Indigo 是一个基于 SoapMessage 的通信模型,通过节点(端口)之间的推送机制工作。从服务角度来看,服务之间的消息对话是完全透明的,如果没有日志记录机制,调试起来会非常困难。本文介绍了一个在控制台宿主进程中发布的 SoapMessages 的 Tiny Logger。在许多情况下,如 MSDN Indigo 示例,控制台程序是一个简单的宿主进程,没有任何额外的功能,特别是在服务器端,那么为什么它不能用于记录来自/到软件总线的消息呢?让我们看看如何在 Indigo 中实现它。
特点
Tiny Indigo Logger 具有以下功能:
- 记录到端口的传入消息
- 记录从端口传出的消息
- 在控制台宿主进程中发布消息
- XML 文本格式的输出布局
- 通过宿主进程(或计算机)配置文件实现的松散耦合插件
- 以编程方式插入端口
以下屏幕截图显示了在 Indigo BothWayServiceMessageSession 示例(服务器端)中打开的端口上的对象。
概念
消息日志记录概念基于将发布者处理程序注入端口管道。下图显示了在 Spy 阶段(第一个 PortExtension
处理程序)之前捕获接收端口中的消息。
来自 Transporter 的传入 SoapMessage 流向 ConsolePublisher(Spy 的别名阶段),在那里被捕获并转储到控制台屏幕,然后转发到原始 Spy 的阶段处理程序,依此类推。捕获的消息被着色,使其与应用程序驱动的消息区分开来。每条消息都有自己的唯一 ID,基于通信服务模式,例如 MessageId
、RelatesTo
、SequenceId
、SequenceAckowledgementId
等。此唯一 ID 在控制台屏幕上以粗体显示 - 请参阅上图。
包含的 Tiny Logger 解决方案在端口工作流中的捕获消息位置是硬编码的,但很容易更改为其他位置或根据配置属性添加更多阶段。
用法
Tiny Logger 要求具有控制台宿主进程,并将 PortExtensionLib
程序集安装到 GAC 或应用程序的 bin 文件夹中。
可以使用以下方式之一将 ConsoleLoggingPort
扩展注入端口:
- 紧耦合 - 在端口打开之前,在宿主程序中创建
ConsoleLoggingPort(Port port)
或ConsoleLoggingPort(Port port ConsoleLoggingSettings settings)
的实例。 - 松散耦合 - 插入到宿主进程 app.config 文件的
<serviceEnvironment>
部分 - 请参阅下面的 Indigo 示例的配置片段,元素<rk.logging mode="on" color="on" details="true" setconsole="true"/>
。<configuration> <system.messagebus> <!-- <configurationHandlers> <add name="serviceEnvironmentItems"> <add name="rk.logging" type="RKiss.PortExtensionLib.ConsoleLoggingPortConfigHandler, RKiss.PortExtensionLib" /> </add> </configurationHandlers> --> <serviceEnvironments> <serviceEnvironment name="main"> <port> <identityRole> soap.tcp://:46001/HelloClient/</identityRole> </port> <!-- CAUTION: Security disabled for demonstration purposes only. --> <remove name="securityManager" /> <policyManager> <!-- CAUTION: Security disabled for demonstration purposes only. --> <!-- Permits unsigned policy statements. Default requires signed policy statements --> <areUntrustedPolicyAttachmentsAccepted>true </areUntrustedPolicyAttachmentsAccepted> <isPolicyReturned>true</isPolicyReturned> </policyManager> <rk.logging mode="on" color="on" details="true" setconsole="true" /> </serviceEnvironment> </serviceEnvironments> </system.messagebus> </configuration>
请注意,
serviceEnvironmentItem
<rk.logging />
可以像上面注释掉的部分所示那样本地引用,也可以在 machine.config 中全局引用。<add name="serviceEnvironmentItems"> <add name="dialogManager" type="System.MessageBus.Configuration.DialogManagerConfigurationHandler, System.MessageBus, Version=1.2.3400.0, Culture=neutral, PublicKeyToken=b77a5c561934e089" /> ... <add name="rk.logging" type="RKiss.PortExtensionLib.ConsoleLoggingPortConfigHandler, RKiss.PortExtensionLib, Version=1.0.0.0, Culture=neutral, PublicKeyToken=5e79b308cfc5975f" /> </add>
可以通过设置其属性来配置 Console Publisher:
名称 | 默认值 | 注意 |
---|---|---|
模式 |
"on " |
启用/禁用日志记录 |
send |
"true " |
记录传出消息 |
receive |
"true " |
记录传入消息 |
details |
"false " |
记录头信息 |
color |
"on " |
彩色开关 |
setconsole |
"true " |
设置控制台缓冲区和窗口 |
这是一个例子。
<rk.logging mode="on" color="on" details="true" setconsole="true" />
其结果显示以下屏幕截图。
我推荐以下 Tiny Logger 的配置设置:
- 将
PortExtensionLib
程序集安装到 GAC。 - 将
ConsoleLoggingPortConfigHandler
添加到 machine.config 文件。 - 将
<rk.logging />
部分添加到宿主进程配置文件。
请注意,Tiny Logger 没有运行时用户界面。消息被写入控制台循环缓冲区,并基于控制台的标准功能进行管理,如查找、选择、复制等。
实现
Tiny Logger 的实现分为三个部分 - 基于功能的三个文件:
- ConsoleLoggingSettings.cs
基本上,这个类只有一个职责 - 将
ConsoleLoggingPort
类连接到 Port。在调用WireUp
方法之前,服务管理器会调用Create
方法以检索包含在ConsoleLoggingSettings
对象中的可配置属性(其类型通过ServiceComponentType
方法获取)。以下代码片段详细显示了实现:public class ConsoleLoggingPortConfigHandler: IServiceEnvironmentConfigurationHandler { public ConsoleLoggingPortConfigHandler() { Trace.WriteLine("ConsoleLoggingPortConfigHandler ctor"); } public object Create(object parent, object context, XmlNode section) { ConsoleLoggingSettings settings = new ConsoleLoggingSettings(); // Exit if there are no configuration settings. if(section == null) return settings; XmlNode currentAttribute; XmlAttributeCollection nodeAttributes = section.Attributes; // Get the mode attribute. currentAttribute = nodeAttributes.RemoveNamedItem("mode"); if(currentAttribute != null && currentAttribute.Value.ToUpper(CultureInfo.InvariantCulture) == "OFF") { settings.Mode = LoggingMode.Off; } // Get the send attribute. currentAttribute = nodeAttributes.RemoveNamedItem("send"); if(currentAttribute != null) { settings.Send = Convert.ToBoolean(currentAttribute.Value); } // other attributes ... return settings; } public void WireUp(object items, ServiceEnvironment se) { Port port = se[typeof(Port)] as Port; ConsoleLoggingPort clp = new ConsoleLoggingPort(port, items as ConsoleLoggingSettings); Trace.WriteLine("ConsoleLoggingPortConfigHandler WireUp is done."); } public Type ServiceComponentType { get { //register our settings object return typeof(ConsoleLoggingSettings); } } }
- ConsoleLoggingPortEx.cs
这个类负责使用 Indigo 管道阶段策略和
PortExtension
基类支持,将 ConsolePublisher 注入到 Port 管道的适当阶段。ConsoleLoggingPort
类派生自PortExtension
类,这是 Indigo 管道模式所必需的。我们需要创建两个阶段:一个用于我们的 Console Publisher(日志记录),另一个称为别名,代表原始阶段(在我们的 Logger 解决方案中 - 分别是 Transmit 阶段和 Spy 阶段)。Indigo Port 管道模型具有开放的阶段基础设施,因此我们可以创建更多阶段,例如 Transmit 阶段之后的阶段等。请注意,管道阶段策略需要为每个已知阶段都有一个有效的处理程序,否则消息工作流将具有不确定的行为。public class ConsoleLoggingPort : PortExtension { ConsoleLoggingSettings settings; // the logging stages static Stage aliasedStage; static Stage loggingStage; #region Stages used for send pipeline. StageAlias[] sendAliases; Stage[] sendStages; IMessageHandler[] sendHandlers; #endregion #region Stages used for receive pipeline. StageAlias[] receiveAliases; Stage[] receiveStages; IMessageHandler[] receiveHandlers; #endregion #region Constructor // Constructor used by the configuration system, internal only internal ConsoleLoggingPort() : this(null) {} // Most commonly used ctor - defaults values public ConsoleLoggingPort(Port port) : this(port, null) {} // Constructor for config file public ConsoleLoggingPort(Port port, ConsoleLoggingSettings settings) { // config properties this.settings = settings == null ? new ConsoleLoggingSettings() : settings; // We have to create two stages with the following unique names string strGuid = Guid.NewGuid().ToString(); loggingStage = new Stage(new Uri(string.Format("uuid:{0};id=0", strGuid))); aliasedStage = new Stage(new Uri(string.Format("uuid:{0};id=1", strGuid))); if(this.settings.Mode == LoggingMode.On) { if(this.settings.Send) { #region Message workflow stages for Send handlers // The first one is our logging and next one is Transmit. Stage[] aliasedSendStages = new Stage[] { loggingStage, aliasedStage }; // The stage for our logging handler. sendStages = new Stage[] { loggingStage }; // this StageAlias will replace an actualy // Transmit stage in the send pipeline sendAliases = new StageAlias[] { new StageAlias(PortSendStages.Transmit, aliasedStage, aliasedSendStages) }; #endregion #region attach the ConsolePublisher sendHandlers = new IMessageHandler[] {new ConsolePublisher("SEND", this.settings)}; #endregion } if(this.settings.Receive) { #region Message workflow stages for Receive handlers // The first one is our logging and next one is Spy. Stage[] aliasedReceiveStages = new Stage[] { loggingStage, aliasedStage }; // The stage for our logging handler. receiveStages = new Stage[] { loggingStage }; // this StageAlias will replace an actualy // Spy stage in the receive pipeline this.receiveAliases = new StageAlias[] {new StageAlias(PortReceiveStages.Spy, aliasedStage, aliasedReceiveStages) }; #endregion #region attach the ConsolePublisher receiveHandlers = new IMessageHandler[] { new ConsolePublisher("RECEIVE", this.settings) }; #endregion } #region remove and add our extension from the port pipeline if(port.Extensions.Contains(this)) port.Extensions.Remove(this); else port.Extensions.Add(this); #endregion } } #endregion #region PortExtension - overrides public override IMessageHandler[] CreateReceiveHandlers() { return receiveHandlers; } public override IMessageHandler[] CreateSendHandlers() { return sendHandlers; } public override StageAlias[] GetSendAliases() { return sendAliases; } public override Stage[] GetSendStages() { return sendStages; } public override StageAlias[] GetReceiveAliases() { return receiveAliases; } public override Stage[] GetReceiveStages() { return receiveStages; } public override void OnOpening() { #region Logo Console.ForegroundColor = ConsoleColor.Green; Console.WriteLine("********************" + "****************************************"); Console.WriteLine(" Tiny Console Logger Version" + " 0.9, created by Roman Kiss "); Console.WriteLine("********************" + "****************************************"); Console.ResetColor(); #endregion #region Setup Screen if(settings.SetConsole) { Console.SetBufferSize(300, 5000); Console.SetWindowSize(75, 50); } #endregion #region dump all port collections Console.ForegroundColor = settings.Color == ColorMode.On ? ConsoleColor.Cyan : Console.ForegroundColor; Console.WriteLine(">>> The Port has been opened"); Console.ForegroundColor = settings.Color == ColorMode.On ? ConsoleColor.DarkCyan : Console.ForegroundColor; Console.WriteLine("PortExtensions:"); foreach(object obj in this.Port.Extensions) Console.WriteLine(" " + obj.ToString()); Console.WriteLine("PortFormatters:"); foreach(object obj in this.Port.Formatters) Console.WriteLine(" " + obj.ToString()); Console.WriteLine("PortHeaderTypes:"); foreach(object obj in this.Port.HeaderTypes) Console.WriteLine(" " + obj.ToString()); Console.WriteLine("PortTransportAddresses:"); foreach(object obj in this.Port.TransportAddresses) Console.WriteLine(" " + obj.ToString()); Console.WriteLine("PortTransports:"); foreach(object obj in this.Port.Transports) Console.WriteLine(" " + obj.ToString()); Console.WriteLine("==========================" + "================================"); Console.WriteLine(""); Console.ResetColor(); #endregion } public override void OnClosed() { Console.WriteLine("The Port has been closed"); } #endregion }
- ConsolePublisher.cs
这个端点类是我们的消息处理程序。它必须派生自
SyncMessageHandler
基类,才能成为管道消息工作流的一部分。它的实现非常直接,可以使用 XML 文本格式的布局将消息信封(如 Headers 和 Body)写入控制台屏幕。#region Console Publisher // Simple handler that delegates message // to the Console in the xml formatted text class ConsolePublisher : SyncMessageHandler { #region Private Members private string prompt; #endregion #region Constructor ConsoleLoggingSettings settings; public ConsolePublisher(string prompt, ConsoleLoggingSettings settings) : base() { this.prompt = prompt; this.settings = settings == null ? new ConsoleLoggingSettings() : settings; } #endregion #region ProcessMessage public override bool ProcessMessage(Message msg) { #region validation if(msg == null) throw new ArgumentNullException("message"); if(msg.Encoding == null) throw new ArgumentException("message"); #endregion lock(this) { #region Capture message Message message = msg.Clone(); try { #region Prompt Line MessageIdHeader mih = message.Headers[typeof(MessageIdHeader)] as MessageIdHeader; RelatesToHeader rth = message.Headers[typeof(RelatesToHeader)] as RelatesToHeader; MessageHeader WsrmSeq = message.Headers["Sequence", @"http://schemas.xmlsoap.org/ws/2003/02/RM"] as MessageHeader; MessageHeader WsrmSeqAck = message.Headers["SequenceAcknowledgement", @"http://schemas.xmlsoap.org/ws/2003/02/RM"] as MessageHeader; // select properly Id to show up string strId = "Unknown Message Id"; if(mih != null) { strId = mih.MessageId.ToString(); } else if(rth != null) { strId = string.Concat("RelatesTo: " + rth.RelatesTo.ToString()); } else if(WsrmSeq != null) { strId = string.Concat("Sequence: ", WsrmSeq.Element.InnerText.Substring( WsrmSeq.Element.InnerText.LastIndexOf('/') + 1)); } else if(WsrmSeqAck != null) { strId = string.Concat("SequenceACK: ", WsrmSeqAck.Element.InnerText.Substring( WsrmSeqAck.Element.InnerText.LastIndexOf('/') + 1)); } Console.WriteLine(""); Console.ForegroundColor = settings.Color == ColorMode.On ? ConsoleColor.Yellow : Console.ForegroundColor; Console.WriteLine(string.Format(">>> {0} {1}", prompt, strId)); Console.ForegroundColor = settings.Color == ColorMode.On ? ConsoleColor.DarkYellow : Console.ForegroundColor; #endregion #region Message headers foreach(MessageHeader mh in message.Headers) { try { if(settings.Details) { XmlNode node = mh.Element; if(node != null) Console.WriteLine(OutputXmlLayout(node.OuterXml)); } else Console.WriteLine(mh.Name + ": " + mh.StringValue); } catch(Exception ex) { Console.WriteLine(string.Format("[{0}={1}], {2}", mh.Name, mh.StringValue, ex.Message)); } } #endregion #region Message Content Console.WriteLine("-----------" + "--<Message.Content>---------------------------"); XmlReader reader = message.Content.Reader; XmlDocument document = new XmlDocument(reader.NameTable); while(reader.MoveToContent() == XmlNodeType.Element) { try { XmlNode node = document.ReadNode(reader); if(node != null) Console.WriteLine(OutputXmlLayout(node.OuterXml)); } catch(Exception ex) { Console.WriteLine(ex.Message); } } Console.WriteLine("===========" + "=============================================="); Console.WriteLine(""); #endregion } catch(Exception ex) { Console.ForegroundColor = ConsoleColor.Red; Console.WriteLine("Logging Internal Error: {0}", ex.Message); } finally { //clean-up Console.ResetColor(); message.Close(); } #endregion } // follow next handler in the pipeline return true; } #endregion #region Helpers /// <summary>Helper class to layout /// the xml formatted string</summary> /// <param name="strSource"></param> /// <returns></returns> private string OutputXmlLayout(string strSource) { StringBuilder sb = new StringBuilder(); using(StringWriter sw = new StringWriter(sb)) { XmlTextWriter tw = new XmlTextWriter(sw); tw.Indentation = 3; tw.Formatting = Formatting.Indented; // load xml formatted text from source string XmlDocument doc = new XmlDocument(); doc.LoadXml(strSource); // save to the writer doc.Save(tw); tw.Flush(); tw.Close(); } return sb.ToString(); } #endregion } #endregion
结论
在本文中,我向您展示了如何为自定义阶段扩展 Indigo 端口管道。它基于 MSDN Indigo 示例中的 PortExtension 示例。通常,出于测试目的,人们经常使用控制台进程(请参阅 MSDN Indigo 示例)来托管您的服务和/或可远程对象。只需将 Tiny Logger 插入宿主进程配置,您就可以在控制台屏幕上看到服务与其使用者之间的 Indigo 对话模式。希望您会喜欢它。
附录
- [1] Longhorn SDK (Indigo Samples)
- [2] Indigo newsgroup
- [3] A Guide to Developing and Running Connected Systems with Indigo.
- [4] On the road to Indigo
- [5] Indigo Hub
- [6] Writing Asynchronous, Bidirectional, Stateful, Reliable Web Services with Indigo.
- [7] Digging into Channel Types
- [8] Inside "Indigo," Chapter 2: The Journey of a Message
- [9] Longhorn Indigo - MSDN.
- [10] Creating Indigo Applications with the PDC Release of Visual Studio .NET Whidbey
- [11] Introducing Microsoft WinFX written by Brent Rector, published by Microsoft Press, chapter 6 (Communication).