在 .NET Remoting 中链接通道






4.56/5 (9投票s)
2002 年 8 月 5 日
12分钟阅读

125735

1193
本文介绍如何使用逻辑 URL 地址连接性来设计和实现通过链接通道(标准和自定义)的远程处理。
目录
引言
现代应用程序架构要求将分布式面向对象设计模型透明地映射到内联网和 Internet 网络上已部署的物理模型。标准的 .NET Remoting 允许仅通过一个标准通道(如 TCP 或 HTTP)来使用远程处理对象,这符合物理点对点设计模式。 .NET Remoting 基础结构没有直接的机制来链接和重新路由消费者与远程对象之间的通道。
本文展示了如何设计和实现消息接收器(路由器)以将远程消息(IMessage
)转发到正确的通道。可以通过其 URL(统一资源定位符)地址描述的已知远程对象的连接性可以映射到逻辑 URL 地址,并在配置文件中进行管理,或者在运行时以编程方式更改。使用逻辑 URL 地址可以基于远程接口约定来虚拟化分布式业务模型。
在我们深入研究其实现细节之前,先从概念和用法开始。我假设您对 .NET Remoting 有所了解。
概念
要创建由接口约定驱动的已知远程对象的代理,需要了解以下信息:
- 描述接口约定的元数据,例如:
ITestInterface
- 远程对象的 URL 地址,例如:tcp://:9090/endpointB
接口约定是位于 GAC 中共享程序集中的消费者与远程对象之间的抽象定义,它允许构建基于松耦合设计模式的分布式模型。
第二个要求是 URL 地址代表点对点连接性的物理描述。此字符串可以硬编码,也可以通过额外的代码以编程方式从自定义配置文件中检索。
基本上,URL 地址包含与远程处理通道和远程处理对象相关的信息。这是关于消息分派的信息。例如:
tcp://:9090/endpointB
其中
-
tcp 是一个通道名称,用于从当前 AppDomain 中已注册通道的集合中选择指定的通道。这是一个唯一的名称,它包含组织成堆栈的请求的所有接收器的链接。远程消息会按照接收器注册的顺序流过此接收器堆栈。
-
:// 指定通道分隔符
-
localhost:9090 指定目标(接收者)通道。在我们的示例中,这是一个标准的 TCP 接收器。使用自定义通道,这可能会有所不同,例如:对于 MSMQ 通道,这部分可能看起来像这样:./private$/queueName
-
/ 指定终结点分隔符
-
endpointB 表示 一个对象 URI 地址。这是远程终结点地址,引用由其宿主进程发布的已知远程对象。请注意,终结点地址仅是服务器通道中最后一个接收器所必需的。通道链接的概念基于此。
URL 地址存储在 IMessage
(属性 Uri)中,基于客户端请求。这是一个可读写属性,允许修改并使其更抽象。在正确的通道中,我们需要一个自定义消息接收器来检查其格式并映射到远程处理基础结构所需的物理 URL 地址。
以下接收器描述了这些功能。
客户端路由器
路由器客户端消息接收器的概念基于将物理 URL 地址映射到唯一逻辑名称,例如,请参见以下映射:
tcp://:9090/endpointB => tcp://testobject
其中:testobject = localhost:9090/endpointB 代表客户端的逻辑 URL 名称。
路由器客户端接收器负责基于 URL 地址的内存知识库执行此映射。urlKB 位于客户端接收器提供程序中,并且可以在注册服务时从配置文件中配置。当然,其内容也可以在运行时根据应用程序需求进行更新,这将允许动态重新配置分布式模型。
以下配置文件片段显示了 TCP 通道中客户端提供程序的配置,包括其 urlKB(自定义属性 lurl):
<clientProviders> <!-- Message Router --> <provider ref="router" name="TcpRouterC9092" lurl ="endpoint=localhost:9090/endpoint, endpointB=localhost:9090/endpointB, test=localhost:9092/; tcp://:9090/endpoint, router9092=localhost:9092/"/> <formatter ref="binary" /> </clientProviders>
服务器路由器
路由器服务器消息接收器在逻辑通道中的位置与其客户端不同。服务器通道正在处理客户端的传出 IMessage
,因此路由器消息接收器可以根据 URL 地址控制消息工作流。
通道链接的远程处理通道概念如图所示:
路由器由逻辑 URL 地址字符串中的分隔符字符 ';' 驱动。在这种情况下,IMessage
被分派到下一个通道,而不是转发到下一个接收器。与上面的客户端提供程序一样,服务器提供程序也包含 urlKB 以获取物理 URL 地址。
以下配置文件片段显示了这一点:
<serverProviders> <formatter ref="binary" /> <provider ref="router" name="TcpRouterS9092" lurl ="router9090=tcp://:9090/, router9092=tcp://:9092/, endpoint=tcp://:9090/endpoint, endpointB=tcp://:9090/endpointB, test=tcp://:9092/; tcp://:9090/endpoint"/> </serverProviders>
基于上述概念,与远程对象的连接性是透明的,无论链接了多少通道。此连接性可以由唯一的逻辑名称描述,并且每个通道的路由器将负责处理。
用法
使用路由器接收器要求您将 MessageRouter.dll 和 RouterLogicalCallContext.dll 程序集安装到 GAC 中,并对 remoting 部分的 machine.config 文件进行以下修改:
<channelSinkProviders> <clientProviders> <formatter id="soap" type="System.Runtime.Remoting.Channels.SoapClientFormatterSinkProvider, System.Runtime.Remoting,Version=1.0.3300.0, Culture=neutral, PublicKeyToken=b77a5c561934e089"/> <formatter id="binary" type="System.Runtime.Remoting.Channels.BinaryClientFormatterSinkProvider, System.Runtime.Remoting,Version=1.0.3300.0, Culture=neutral, PublicKeyToken=b77a5c561934e089"/> <provider id="router" type="RKiss.MessageRouter.RouterClientSinkProvider, MessageRouter,Version=1.0.936.36529, Culture=neutral, PublicKeyToken=47a36cf75249d9dc"/> </clientProviders> <serverProviders> <formatter id="soap" type="System.Runtime.Remoting.Channels.SoapServerFormatterSinkProvider, System.Runtime.Remoting,Version=1.0.3300.0, Culture=neutral, PublicKeyToken=b77a5c561934e089"/> <formatter id="binary" type="System.Runtime.Remoting.Channels.BinaryServerFormatterSinkProvider, System.Runtime.Remoting,Version=1.0.3300.0, Culture=neutral, PublicKeyToken=b77a5c561934e089"/> <provider id="wsdl" type="System.Runtime.Remoting.MetadataServices.SdlChannelSinkProvider, System.Runtime.Remoting,Version=1.0.3300.0, Culture=neutral, PublicKeyToken=b77a5c561934e089"/> <provider id="router" type="RKiss.MessageRouter.RouterServerSinkProvider, MessageRouter,Version=1.0.936.36529, Culture=neutral, PublicKeyToken=47a36cf75249d9dc"/> </serverProviders> </channelSinkProviders>
MessageRouter 包含两个接收器提供程序,一个用于客户端,另一个用于服务器。两者都具有相同的 ID(router),可用于在配置文件中引用它们。
以下功能是内置的:
- RouterClientSinkProvider 仅用于将逻辑 URL 地址映射到物理 URL。
- RouterServerSinkProvider 用于映射 URL 地址并重新路由远程消息流。
请注意,客户端/服务器通道中的路由器不是紧密耦合的,因此可以根据应用程序需求单独使用它们。
在安装 MessageRouter(GAC + machine.config)之后,路由器就可以在 .Net Remoting 基础结构中像 soap 和 binary 等其他标准提供程序一样使用。
下图说明了如何使用客户端路由器:
远程对象(发布为 endpointB)对客户端来说在松耦合设计模式下是完全透明的。客户端根据唯一的逻辑 URL 地址(例如: endpointB)在 TCP 通道中创建其代理。客户端路由器将此地址映射到物理地址(tcp://:9090/endpointB)并转发到下一个接收器。请注意,逻辑 URL 名称不必与终结点地址匹配,但使用相同的名称会更好且更具可读性。
正如您所看到的,使用客户端路由器非常直接且可配置,特别是当客户端基于远程接口约定设计模式时。
那么,当部署模型需要使用多个通道时(例如:通过 Web 服务和 MSMQ 进行异步远程调用,使用自定义 MSMQ 通道进行事件驱动架构等)会怎样?在这种情况下,链接通道是隐藏业务逻辑中所有连接性问题的绝佳解决方案。
下图显示了使用服务器路由器进行通道链接的配置问题:
通道 X-1 的服务器路由器在 Uri 字符串中找到路由器分隔符(';'),这指示将 IMessage
转发到下一个通道(通道 X)。Uri 字符串中的下一个通道由其唯一的逻辑 URL 地址描述,因此路由器将从其知识库(urlKB)中将其替换为物理地址。之后,IMessage
可以重新路由到正确的通道。
请注意,链接的通道可以以标准和自定义通道的任意组合使用。为了说明和评估目的,我使用了标准 TCP 通道的链接。
更新路由器
前面提到,路由器接收器(urlKB)的知识库可以在运行时更新。该概念基于使用 CallContext
对象,该对象随远程消息在客户端和远程对象之间传输。在 RouterLogicalCallContext.dll 程序集中有一个非常简单的抽象定义(约定)。
namespace RKiss.MessageRouter
{
[Serializable]
public class RouterLogicalCallContext : ILogicalThreadAffinative
{
string strUrlKB;
public string UrlKB
{
get {return strUrlKB; }
set { strUrlKB = value; }
}
}
}
以下客户端代码片段显示了如何更新 urlKB,例如,在 TcpRouterS9092 中:
//update a local knowledge base of the url addresses
routerName = "TcpRouterS9092";
RouterLogicalCallContext urlkb = new RouterLogicalCallContext();
urlkb.UrlKB = "endpointB=tcp://:1234/myObjectUri, endpoint=";
CallContext.SetData(routerName, urlkb);
上面的代码将执行更新 endpointB 条目并删除 endpoint 条目,从 TcpRouterS9092 的 urlKB 中删除。
配置路由器提供程序
路由器提供程序使用以下标准和自定义属性:
ref
是被引用的提供程序模板(例如:ref="router")name
指定提供程序的名称。每个提供程序都有一个唯一的名称,用于正确传递消息(例如:name="TcpRouterC9092")lurl
(自定义属性)** 指定 URL 对的字符串,例如逻辑 URL 名称及其物理表示。逗号是分隔符,用于分隔 lurl 字符串中的每个 URL 对(例如: test=tcp://:9092/; tcp://:9090/endpoint, endpoint=tcp://:9090/endpoint)。
设计
路由器设计基于将 IMessage
重定向到下一个链接通道的第一个消息接收器。.NET Remoting 基础结构允许在通道中链接消息接收器。在适当的通道位置(消息接收器堆栈)插入自定义消息接收器(路由器)并监视 URL 地址是通道链接的设计模式。
下图显示了这一点:
服务器通道中的传入 IMessage
首先通过格式化器。之后,下一个接收器是路由器,IMessage
被传递给它的 ProcessMessage
方法。此处包含所有路由器逻辑:
- 检查
CallContext
是否包含路由器名称。如果对象存在,则根据其属性(UrlKB)更新路由器的知识库。 - 检查 URL 地址字符串中的路由器分隔符。如果不存在,则调用标准消息流 -
nextSink.ProcessMessage
方法 ,并将其结果返回给调用者。
在存在路由器分隔符的情况下,将执行以下路由器逻辑:
- 如果需要,将从本地 URL 知识库中替换逻辑 URL 地址。
- 搜索下一个(链接的)通道。
- 为此通道创建 MessageSink。
- 调用
SyncProcessMessage
或AsyncProcessMessage
方法将IMessage
传递到下一个通道(第一个消息接收器)。 - 将结果返回给调用者。
如上图所示,通道链接是一种非常直接的方式,性能开销最小。IMessage
映像已由远程对象的消费者创建,我们只需将其重定向到链接通道中的另一个接收器。
消息流的行为与客户端通道类似,因此链接通道必须在同一宿主进程中与路由器通道一起注册。在这种情况下,宿主进程代表链接的远程处理通道之间的桥接进程。链接通道不必是同一类型,它可以是任何标准或自定义通道,.NET Remoting 基础结构将保证 IMessage
将正确地流过所有这些通道。
实现
路由器的实现使用了标准的客户端消息接收器样板(基础结构)。我将跳过这部分描述,只关注与路由器相关的内容。
服务器路由器
在服务器路由器中,有两个地方需要插入路由器逻辑。第一个地方是接收器提供程序的构造函数,用于通过配置文件中的值初始化其知识库(位于哈希表对象中)。以下代码片段显示了这一点:
public RouterServerSinkProvider(IDictionary properties,
ICollection providerData)
{
string strLURL = "";
if(properties.Contains("name"))
m_strProviderName = Convert.ToString(properties["name"]);
if(properties.Contains("lurl"))
strLURL = Convert.ToString(properties["lurl"]);
if(strLURL != "")
{
try
{
string[] arrayUrl = strLURL.Split(new char[]{'=',','});
for(int ii=0; ii<arrayUrl.Length; ii++)
{
m_HT.Add(arrayUrl[ii].Trim(), arrayUrl[++ii].Trim());
}
}
catch(Exception ex)
{
string strWarning =
string.Format(
"{0}.RouterServerSinkProvider has problem ({1}) in the {2}.",
m_strProviderName, ex.Message, strLURL);
WriteEventLog(strWarning, EventLogEntryType.Warning);
}
}
WriteEventLog(string.Format(
"{0}.RouterServerSinkProvider has been initiated.",
m_strProviderName));
}
第二个地方是接收器的 ProcessMessage
方法。其中有一个逻辑,用于根据针对指定路由器的 CallContext
对象更新内部知识库(哈希表)。其余工作在私有方法(如 MessageDispatcher
和 MessageRouter
)中完成。
public ServerProcessing ProcessMessage(
IServerChannelSinkStack sinkStack,
IMessage requestMsg, ITransportHeaders requestHeaders,
Stream requestStream, out IMessage responseMsg,
out ITransportHeaders responseHeaders,
out Stream responseStream)
{
ServerProcessing servproc = ServerProcessing.Complete;
responseHeaders = null;
responseStream = null;
//Are we in the business?
if(m_Next != null)
{
//check the Router Call Context
object objCC = requestMsg.Properties["__CallContext"];
if(objCC != null && objCC is LogicalCallContext)
{
LogicalCallContext lcc = objCC as LogicalCallContext;
object objData = lcc.GetData(m_Provider.ProviderName);
if(objData != null && objData is RouterLogicalCallContext)
{
RouterLogicalCallContext rlcc =
objData as RouterLogicalCallContext;
if(rlcc.UrlKB == "")
{
//clear the local KB
m_Provider.ClearLogicalURL();
}
else
if(rlcc.UrlKB == "?")
{
//retrieve the local KB contens
rlcc.UrlKB = m_Provider.GetLogicalURL();
lcc.SetData(m_Provider.ProviderName, rlcc);
}
else
{
//update local knowledge base
string[] arrayUrl =
rlcc.UrlKB.Split(new char[]{'=',','});
for(int ii=0; ii<arrayUrl.Length; ii++)
{
m_Provider.SetLogicalURL(
arrayUrl[ii].Trim(), arrayUrl[++ii].Trim());
}
}
}
}
//Dispatch message
responseMsg = MessageDispatcher(requestMsg);
if(responseMsg == null)
{
//processing message in the current channel
servproc = m_Next.ProcessMessage(sinkStack, requestMsg,
requestHeaders, requestStream,
out responseMsg, out responseHeaders, out responseStream);
}
else
if(RemotingServices.IsOneWay((
requestMsg as IMethodCallMessage).MethodBase) == true)
{
servproc = ServerProcessing.OneWay;
}
}
else
{
//---We have no active sink
Trace.WriteLine(string.Format(
"{0}:RouterServerSink ProcessMessage null",
m_Provider.ProviderName));
responseMsg = null;
responseHeaders = null;
responseStream = null;
}
return servproc;
}
MessageDispatcher
方法负责验证主 URL 地址。在逻辑地址的情况下,它将执行其到物理形式的映射,使用提供程序的知识库(urlKB)。
private IMessage MessageDispatcher(IMessage requestMsg)
{
IMessage responseMsg = null;
if(requestMsg.Properties["__Uri"] != null)
{
string strUrl = requestMsg.Properties["__Uri"].ToString();
//try to split the url adresses and primary address
string[] strArrayUrlPath = strUrl.Split(';');
string[] strArrayPrimaryAddr = strArrayUrlPath[0].Split('/');
//checking the endpoint?
if(strArrayUrlPath.Length == 1 &&
strArrayPrimaryAddr.Length == 2)
{
//Yes, it is. The message is going to forward
//it to the StackBuilder.
//do nothing, here (responseMsg is null).
}
else
{
//get the new primary address
string strObjUrl = strUrl.Remove(0,
strArrayUrlPath[0].Length + 1).TrimStart(' ');
string[] strArrayNewUrlPath =
strObjUrl.Split(new char[]{';'}, 2);
string lurl = strArrayNewUrlPath[0].Trim();
//is this a logical Url
if(lurl.IndexOf("://") < 0)
{
//yes, it is. Replace this logical
//uri by its physical mapping
string purl = m_Provider.GetLogicalURL(lurl);
if(purl != "" && strArrayNewUrlPath.Length == 1)
{
strObjUrl = purl;
}
else
if(purl != "" && strArrayNewUrlPath.Length == 2)
{
strObjUrl = purl + ";" + strArrayNewUrlPath[1];
}
}
//call router (forwarding the IMessage to the properly channel)
responseMsg = MessageRouter(requestMsg, strObjUrl);
}
}
else
{
//The url address can not by empty
Exception exp = new Exception(
string.Format(
"{0}:RouterServerSink: The Uri address is null",
m_Provider.ProviderName));
responseMsg = new ReturnMessage(exp,
(IMethodCallMessage)requestMsg);
}
return responseMsg;
}
最后,重路由过程在以下方法中实现。逻辑非常简单。首先,搜索有效通道并根据链接的物理 URL 地址创建其第一个消息接收器。当我们拥有正确的消息接收器时,就可以将 IMessage
传递到接收器中,调用其 SyncProcessMessage
或 AsyncProcessMessage
方法。返回值(respondMsg
)被发送回原始调用者。
private IMessage MessageRouter(IMessage requestMsg, string strObjUrl)
{
IMessage responseMsg = null;
//Redirect the IMessage to the properly outgoing channel
requestMsg.Properties["__Uri"] = strObjUrl;
string strDummy = null;
IMessageSink iMsgSink = null;
//find the properly outgoing channel registered in this process
foreach(IChannel channel in ChannelServices.RegisteredChannels)
{
if(channel is IChannelSender)
{
iMsgSink = (channel as IChannelSender).CreateMessageSink(
strObjUrl, null, out strDummy);
if(iMsgSink != null)
break;
}
}
//check our result
if(iMsgSink == null)
{
//Sorry we have no properly channel to the target object
string strErr = string.Format(
"{0}:RouterServerSink: A supported " +
"channel could not be found for {1}",
m_Provider.ProviderName, strObjUrl);
responseMsg = new ReturnMessage(new Exception(strErr),
(IMethodCallMessage)requestMsg);
}
else
{
//Pass the IMessage to the following channel
//based on the method's attribute
//The SyncProcessMessage can not be done on the
//OneWay attributed method (deadlock process)
if(RemotingServices.IsOneWay((requestMsg
as IMethodCallMessage).MethodBase) == true)
{
responseMsg = (IMessage)iMsgSink.AsyncProcessMessage(
requestMsg, null);
}
else
{
responseMsg = iMsgSink.SyncProcessMessage(requestMsg);
}
}
return responseMsg;
}
客户端路由器
客户端路由器实现的实现比服务器端简单得多。更新路由器知识库并将逻辑 URL 地址映射到物理地址的方式与服务器路由器相同。唯一的区别在于处理的位置。对于客户端路由器,正确的位置是 CreateSink
方法。请注意,在创建远程对象的代理后,最终的 URL 地址将传递给消息接收器。
public IClientChannelSink CreateSink(IChannelSender channel,
string url, object remoteChannelData)
{
IClientChannelSink Sink = null;
m_strChannelName = channel.ChannelName;
StringBuilder sbUrl = new StringBuilder(m_strChannelName);
sbUrl.Append("://");
try
{
//check the Router Call Context
object obj = CallContext.GetData(ProviderName);
if(obj != null && obj is RouterLogicalCallContext)
{
RouterLogicalCallContext rlcc =
obj as RouterLogicalCallContext;
if(rlcc.UrlKB == "")
{
//clear the local KB
ClearLogicalURL();
}
else
if(rlcc.UrlKB == "?")
{
//retrieve the local KB contens
rlcc.UrlKB = GetLogicalURL();
CallContext.SetData(ProviderName, rlcc);
}
else
{
//update local knowledge base
string[] arrayNewUrl =
rlcc.UrlKB.Split(new char[]{'=',','});
for(int ii=0; ii<arrayNewUrl.Length; ii++)
{
SetLogicalURL(arrayNewUrl[ii].Trim(),
arrayNewUrl[++ii].Trim());
}
}
}
//replace logical uri address by physical
//one from the local KB
string[] arrayUrl = url.Split(new char[]{';'}, 2);
string lurl = arrayUrl[0].Remove(0, sbUrl.Length).Trim();
if(lurl.IndexOf('/') < 0)
{
string purl = GetLogicalURL(lurl);
if(purl != "")
{
sbUrl.Append(purl);
if(arrayUrl.Length == 2)
{
sbUrl.Append(";");
sbUrl.Append(arrayUrl[1]);
}
url = sbUrl.ToString();
}
}
//create a router sink object
object ms = m_Next.CreateSink(channel, url,
remoteChannelData);
Sink = new RouterClientSink(this, url, ms);
}
catch(Exception ex)
{
WriteEventLog(string.Format(
"{0}/{1}.CreateSink catch {2}", m_strChannelName,
m_strProviderName, ex.Message), EventLogEntryType.Error);
}
return Sink;
}
以下代码片段显示了客户端消息接收器中消息处理方法的实现。IMessage.Uri
属性被 m_Uri
的值覆盖,该值代表物理 URL 地址。客户端接收器只是将 IMessage
传递给下一个接收器。
public IMessageCtrl AsyncProcessMessage(IMessage msgReq,
IMessageSink replySink)
{
msgReq.Properties["__Uri"] = m_Url;
IMessageCtrl iMsgCtrl =
m_NextMsgSink.AsyncProcessMessage(msgReq,
replySink);
return iMsgCtrl;
}
public IMessage SyncProcessMessage(IMessage msgReq)
{
msgReq.Properties["__Uri"] = m_Url;
IMessage msgRsp = m_NextMsgSink.SyncProcessMessage(msgReq);
return msgRsp;
}
测试
我创建了一个测试解决方案来测试客户端和服务器路由器。有一个以下项目的集合:
- MessageRouter - 客户端和服务器路由器
- RouterLogicalCallContext - 路由器调用上下文对象的抽象定义
- TestInterface - 测试接口约定
- TestObject - 测试远程对象
- ConsoleServer - 发布测试远程对象的宿主进程
- WindowsClient - 客户端测试器
请注意,最后 4 个项目仅为测试目的而设计和实现,使用了“Hello world”设计模式。
- 将以下程序集安装到 GAC:MessageRouter、RouterLogicalCallContext、TestInterface 和 TestObject。
- 如前所述,修改您的 machine.config 文件。
- 启动 ConsoleServer.exe 程序。
- 启动 WindowsClient.exe 程序。
- 选择顶部组合框中的 URL 地址(参见下图):
- 点击 SayHello。
- 在 ConsoleServer 程序框中检查响应。
URL 组合框下拉菜单包含许多不同的 URL 地址,尝试使用它们来验证客户端/服务器路由器的配置。您可以使用 msg 组合框动态更新客户端/服务器路由器的知识库,以查看映射和路由消息。
结论
本文介绍了一种在 .NET Remoting 中链接通道的简单解决方案。使用逻辑 URL 地址来描述已部署应用程序中的远程连接性,可以大大改进您的客户端实现,并可以在配置文件中或动态地以编程方式进行管理。最大的好处是在基于远程接口约定设计模式的应用程序模型中使用客户端路由器。无需实现额外的代码来检索 GetObject
方法请求的特定 URL 地址,客户端路由器将像使用 new
运算符一样,根据您的配置来处理此映射。链接通道(标准和自定义)将使远程对象的连接性透明化。