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

可扩展的 .NET Remoting 框架

starIconstarIconstarIconstarIcon
emptyStarIcon
starIcon

4.84/5 (22投票s)

2003 年 5 月 1 日

12分钟阅读

viewsIcon

109524

downloadIcon

1667

本文介绍了可安装的传输连接

摘要

.NET Remoting 框架的可扩展性仅限于传输层。有充分的理由让 Remoting 框架通过 TCP 或 HTTP 以外的传输方式进行通信。由于传输层是面向流的,因此在 .NET Remoting 框架中也可以使用命名管道或 RS232 等其他传输方式。本文介绍了一个通用的通道框架,可以轻松添加任何传输方案。

引言

.NET Remoting 框架的设计就是为了可扩展。客户端应用程序可以通过通道发送和接收消息来与远程服务器通信。这些通道由消息接收器(message sinks)组成,它们被链接在一起。消息在传输到服务器之前,会从一个接收器传递到另一个接收器。Remoting 框架允许添加自定义消息接收器以及替换现有的接收器。这样,消息就会被拦截并最终得到处理。

通道通常由两个消息接收器组成:格式化器接收器(formatter sink)和传输接收器(transport sink)。我们可以通过将其他消息接收器链接到格式化器接收器之前或之后来轻松添加它们。事实上,我们还可以用自己的格式化器接收器替换现有的格式化器接收器。

有一点我们无法做到,那就是替换传输接收器。Remoting 框架目前只提供 TCP 和 HTTP 作为传输方式。但我们可以例如将 SOAP 格式化器与 TCP 传输结合,将二进制格式化器与 HTTP 传输结合。对于任何其他类型的传输,例如安全套接字、命名管道或 RS232(仅举几例),则必须从头开始构建一个全新的通道类型。但这并不是一项非常困难的任务。开发人员已经基于替代传输方式创建了消息通道。

在本文中,我将介绍一个通用的通道框架,用于轻松添加替代的传输方式。

传输问题

在客户端,传输通道接收器是接收器链中的最后一个接收器,而在服务器端,它是启动接收器链的第一个接收器。当消息到达客户端传输通道接收器时,它必须已经被格式化或序列化为流,准备好传输到服务器。服务器传输通道接收器接收到序列化消息后,将其传递给接收器链进行反序列化和方法调用。通道架构如下图所示。客户端应用程序通过代理与远程对象通信。对于每个方法调用,代理会形成一个消息对象,并将其向下传递给通道中的下一个接收器。在那里,它被格式化或序列化为字节流。该字节流被传递给传输接收器,然后通过连接传输到服务器。在服务器端接收到字节流后,传输接收器将其转发给格式化器,在格式化器中,消息对象被反序列化,然后转换为对实际对象的相应方法调用。

由于替代传输方式涉及替代连接,因此我设计了通用通道框架来接受可安装的传输连接。连接对象不是通道接收器,而是通道传输接收器使用的某个对象。

连接层由实现一组接口的若干对象组成。假定连接是基于流的,这反映在基本的 ITransportStream 接口中。客户端和服务器通道的传输接收器使用它来发送和接收消息流。

public interface ITransportStream {
    void Read(out ITransportHeaders headers, out Stream stream);
    void Write(IMessage msg, ITransportHeaders headers, Stream stream);
    void Flush();
    void Close();
}

传输流由客户端和服务器的传输连接对象创建并返回。调用远程方法会生成一个消息,该消息最终必须由客户端传输通道接收器处理。因此,在发送任何消息到服务器之前必须建立连接。成功的连接会返回一个传输流。

public interface IClientTransportConnection {
    ITransportStream Connect(String uri);
}

以下代码片段说明了客户端传输接收器如何使用它。

// open the stream
ITransportStream transportStream = _transportConnection.Connect(_channelURL);

// send the request
transportStream.Write(msg, requestHeaders, requestStream);
transportStream.Flush();

// read the response
transportStream.Read(out responseHeaders, out responseStream);

// close the stream
transportStream.Close();

服务器传输接收器会持续监听客户端连接。

public interface IServerTransportConnection {
    ITransportStream Accept();
}

一旦接受了客户端连接,它也会返回一个传输流对象。然后,它开始接收消息数据,并将其转发给接收器链。以下代码片段对此进行了说明。

// read message data
transportStream.Read(out requestHeaders, out requestStream);
...
// invoke the object
ServerProcessing processing = NextChannelSink.ProcessMessage(sinkStack, 
    null, requestHeaders, requestStream, out responseMessage, 
    out responseHeaders, out responseStream);
...
// handle response
transportStream.Write("",responseHeaders, responseStream);
transportStream.Flush();

实现

传输连接和传输流可以为任何面向流的传输技术、套接字、命名管道、RS232 等实现。附带的项目包含两个实现:一个用于命名管道,另一个用于套接字。这里需要注意几点。客户端和服务器传输接收器用于每个远程方法调用和接受,用于从任何客户端到任何服务器。这应该要求对现有连接进行高效的重用。客户端传输接收器只拥有对客户端传输连接的一个引用。因此,您需要自行在传输连接对象内部提供一个可能的开放套接字、管道句柄等的池。服务器传输接收器会持续循环,假设同一连接上会发送一个接一个的消息。一旦响应数据被刷新,它将继续读取另一个请求。因此,您需要确保在没有其他数据可用时,重复的读取请求会超时。当超时到期时,您应该抛出异常。这是一个 transportStream.Read() 方法的实现。

public override void Read(out ITransportHeaders headers, out Stream stream) {
    headers = null;  stream = null;
    _socket.Receive(buffer, 0, 0, SocketFlags.Peek);
  
    if(_socket.Available > 0)
        base.Read(out headers, out stream);
    else
        throw new Exception();

每个新请求的提供方式是首先读取传输头和消息数据流。在读取尝试超时时,假定没有数据可用,并抛出异常。此异常会在请求方法内部被捕获,然后终止重复请求的循环。

try {
    // start a loop of repeated requests
    while(true) {
        // get the request, may throw an exception here
        transportStream.Read(out requestHeaders, out requestStream);
    
        // invoke the object
        ServerProcessing processing = NextChannelSink.ProcessMessage(sinkStack, 
            null, requestHeaders, requestStream, out responseMessage, 
            out responseHeaders, out responseStream);

        // handle response
        if(processing == ServerProcessing.Complete) {
            transportStream.Write(null, responseHeaders, responseStream);
            transportStream.Flush();
        }
    }
} 
catch(Exception exception) {
    if(exception is RemotingException)
        throw exception;
    // this just breaks the loop of repeated requests
    exception = exception; 
} 
finally {
    transportStream.Dispose();
}

这段代码是简化的版本,仅用于展示请求是如何结束的。

通道传输接收器

通道传输连接会插入到通用的通道传输接收器中。您可能会发现使用自己的传输接收器版本有充分的理由。通用通道框架允许您这样做。您可以编写自己的通道传输接收器,并将它们插入,就像插入传输连接一样,前提是服务器通道传输接收器实现了以下 Interface

public interface IServerTransportSink : IServerChannelSink {
    void StartListening(Object data);
    void StopListening(Object data);
}

StartListeningStopListening 方法也作为 IChannelReceiver 接口的一部分。大多数自定义服务器通道实现会将监听代码直接放入 ChannelReceiver 中。但是,由于服务器传输接收器是接收请求的第一道关卡,因此将监听代码放在那里更有意义。您可以参考实现了 IServerTransportSink 接口的通用服务器通道以获得指导。

使用和配置文件

要设置配置文件中的通道部分,您需要指定通道类型以及传输连接或传输接收器。下面是两个通道的示例。

<channel
    type = "dotNET.Remoting.Channels.GenericChannel, GenericChannelsLibrary"
    name = "transport connection channel"
    server-transport-connection = "dotNET.Remoting.Transports.
        PipeServerTransportConnection, TransportStreams.dll"
    client-transport-connection = "dotNET.Remoting.Transports.
        PipeClientTransportConnection, TransportStreams.dll" />
</channel>
<channel
    type = "dotNET.Remoting.Channels.GenericChannel, GenericChannelsLibrary"
    name = "transport sink channel"
    server-transport-sink = "dotNET.Remoting.Transports.
        SoccketServerTransportSink, TransportSinks.dll"
    client-transport-sink = "dotNET.Remoting.Transports.
        SocketClientTransportSink, TransportSinks.dll" />
</channel>

通用通道框架

还有一些其他细节需要考虑。不支持默认的格式化器接收器。您需要指定您想要的格式化器。这是一个示例通道部分。

<channel
    type = "dotNET.Remoting.Channels.GenericChannel, GenericChannelsLibrary"
    name = "transport connection channel"
    server-transport-connection = "dotNET.Remoting.Transports.
        PipeServerTransportConnection, TransportStreams.dll"
    client-transport-connection = "dotNET.Remoting.Transports.
        PipeClientTransportConnection, TransportStreams.dll" />

    <clientProviders>
    <formatter ref="binary" />
    </clientProviders>
    <serverProviders>
    <formatter ref="binary" />
    </serverProviders>
</channel>

服务器通道和/或双向通道必须提供一个本地主机 URL。在基于套接字的通道中,这将是端口号;在基于命名管道的通道中,这将是管道名称。通用通道框架期望本地主机 URL 的形式为:<协议>://<主机名>:<端口/名称>。基于 TCP 套接字通道的本地主机 URL 可能是:tcp://:8000。而对于基于命名管道的通道,则是:pipe://:mypipe。本地主机 URL 必须添加到通道部分。

<channel
    type = "dotNET.Remoting.Channels.GenericChannel, GenericChannelsLibrary"
    name = "transport connection channel"
    localhost-url = "pipe://:mypipe"
    server-transport-connection = "dotNET.Remoting.Transports.
        PipeServerTransportConnection, TransportStreams.dll"
    client-transport-connection = "dotNET.Remoting.Transports.
        PipeClientTransportConnection, TransportStreams.dll">

    <clientProviders>
    <formatter ref = "binary" />
    </clientProviders>
    <serverProviders>
    <formatter ref=";binary" />
    </serverProviders>
</channel>

还有一件事。客户端应用程序可以注册任意数量的通道,甚至多个支持相同传输协议的通道。当需要两个相同传输协议但格式化器不同(二进制和 SOAP)的通道时,这样做是合理的。客户端配置文件中的通道部分可能看起来像这样。

<channel
    type = "dotNET.Remoting.Channels.GenericChannel, GenericChannelsLibrary"
    name = "transport connection channel"
    localhost-url = "pipe://:mypipe"
    server-transport-connection = "dotNET.Remoting.Transports.
        PipeServerTransportConnection, TransportStreams.dll"
    client-transport-connection = "dotNET.Remoting.Transports.
        PipeClientTransportConnection, TransportStreams.dll">

    <clientProviders>
    <formatter ref = "binary" />
    </clientProviders>
    <serverProviders>
    <formatter ref="binary" />
    </serverProviders>
</channel>
<channel
    type = "dotNET.Remoting.Channels.GenericChannel, GenericChannelsLibrary"
    name = "transport connection channel"
    localhost-url = "pipe://:mypipe"
    server-transport-connection = "dotNET.Remoting.Transports.
        PipeServerTransportConnection, TransportStreams.dll"
    client-transport-connection = "dotNET.Remoting.Transports.
        PipeClientTransportConnection, TransportStreams.dll">

    <clientProviders>
    <formatter ref = "soap" />
    </clientProviders>
    <serverProviders>
    <formatter ref="soap" />
    </serverProviders>
</channel>

现在可以通过以下方式获取远程对象的引用。

String url1 = "tcp://server1:9000/MyObject1";
IMyObject1 obj1 = (IMyObject)RemotingServices.Connect(typeof(IMyObject1), url1);

String url2 = "tcp://server2:9090/MyObject2";
IMyObject2 obj2 = (IMyObject)RemotingServices.Connect(typeof(IMyObject2), url2);

您也可以注意到这一点。.NET Remoting 框架不提供指定用于连接远程对象的通道的方法。这对于客户端应用程序来说是一个典型的问题。而在另一端,服务器端,这个问题并不存在。之所以不存在,是因为连接是通过服务器通道的监听器在非常特定的端点建立的。客户端应用程序指定端点,例如 tcp://server1:9000/MyObject1。

通用通道框架的设计考虑到了这一点。因此,在通道部分需要提供更多信息。

<channel 
    type = "dotNET.Remoting.Channels.GenericChannel, GenericChannelsLibrary"
    name = "transport connection channel"
    localhost-url = "pipe://:mypipe" 
    protocol = "soap-tcp"
    server-transport-connection = "dotNET.Remoting.Transports.
        TcpSocketServerTransportConnection, TransportStreams.dll"
    client-transport-connection = "dotNET.Remoting.Transports.
        TcpSocketClientTransportConnection, TransportStreams.dll">
    <clientProviders>
    <formatter ref = "soap" />
    </clientProviders>
    <serverProviders>
    <formatter ref = "soap" />
    </serverProviders>
</channel>

使用通用通道框架似乎非常复杂,因为需要的信息太多了。通过自定义包装通道可以简化这一点。该项目附带了一个这样的库。通道部分可以简化。下面是一个示例。

<channel 
  type = "dotNET.Remoting.Channels.PipeChannel, CustomChannelsLibrary"
  localhost-url = "tcp://:8080" >
</channel>

所有其他信息都可以编码到自定义包装通道的构造函数中。您可以自行查看源代码。

结论

这个项目里还有相当多的代码需要解释。我希望读者熟悉 .NET Remoting 及其可扩展性架构。我希望为 .NET Remoting 和其可扩展性做出有意义的贡献。对我的代码进行批判性审查肯定会有人提出更好的实现建议,特别是在可伸缩性和性能方面。但我更关注架构,而不是其他重要的事情。我确实希望那些对 .NET Remoting 及其可扩展性有浓厚兴趣的人能仔细看看我的工作,并提出更好的建议。请将您的评论发送至:wytek@szymanski.com

附注

微软通过使 .NET Remoting 框架如此可扩展,为世界做出了巨大的贡献。但这不应消除微软改进 .NET Remoting 框架的需求。我想提及一些我遇到的问题。

正如本文前面提到的,应该有一种方法让客户端应用程序指定通信通道,例如

<client>
    <wellknown 
    type = "InterfaceLibrary.IFirstObject, InterfaceLibrary"
    url = "tcp://firstserver:9090/FirstObject"
    channel = "first tcp channel" />
    <wellknown 
    type = "InterfaceLibrary.ISecondObject, InterfaceLibrary"
    url = "tcp://firstserver:9000/SecondObject"
    channel = "second tcp channel" />
<client/>

另外,正如我前面指出的,对于从客户端到服务器的连接,服务器不需要担心通道,因为端点会自动选择正确的通道。但另一个类似的问题出现在服务器回调客户端的情况下。

回调是指作为远程方法调用参数传递的对象的远程引用。例如

void CallMe(SomeObject callback) {
    callback.Call(“Hello”);
}

这段代码在服务器上执行,并且必须通过某个通道进行封送。如果服务器注册了多个通道,它将通过哪个通道进行封送?

当 .NET Remoting 框架为回调对象创建远程对象引用时,它会附加一些通道数据。在通道数据中,有一个 URL 列表,每个 URL 对应一个在客户端注册的通道。然后,在回调时,框架会遍历通道列表,并选择服务器上第一个可用的通道。这就导致了客户端通过 HTTP-SOAP 通道调用服务器,但服务器通过 TCP-二进制通道回调的这种情况。这不是一个理想的效果,并且会使分布式应用程序的开发变得非常复杂。

此外,分布式计算的基本原则是 .NET 框架并不严格遵守的。为什么我们需要将远程程序集部署到客户端机器上?在我看来,这是一个不雅观的事情,而且通过构建接口库、额外的类工厂和其他变通方法并不能使其变得雅观。这与我们期望的 COM/DCOM、CORBA、RPC 和 Java RMI 不同。

否则,我认为 .NET Remoting 框架比任何其他分布式计算框架都要好。但是,有一件事给我留下深刻印象的是 Java RMI 的动态类加载器。在 .NET Remoting 框架中没有类似的东西。这个功能可以开启全新的应用领域。让我简要描述一下 Java RMI 可以做什么。

假设有一个远程 Java 服务器定义如下。

public interface Computable {
    public Object Compute();
}

public interface ComputServer {
    public Object Compute(Computable obj);
}
  
public class ComputeServerImpl extends 
    UnicastRemoteObject implements ComputeServer { 

    public Object Compute(Computable obj) {
        return obj.Compute();
    }
}

而在客户端有一个实现 Computable 接口的对象。

public class ComputableObject extends 
    Serializable implements Computable { 
    
    public Object Compute() { 
    // all the complex computation goes here
    ...
    return this;
}

对象通过值而不是引用被封送到服务器。因此,只有客户端才有可计算对象的类定义,而服务器没有,Java RMI 框架会自动动态地从客户端下载类字节,然后在服务器上执行,这真的很酷。

利用这种功能可以开创分布式计算的新天地。我将其称为分布式代理计算。想象一个分布式计算服务器网络。客户端可能会将代理发送到一个服务器以获取信息,该服务器可能会将请求转发给另一个服务器,依此类推。最终,响应会发送回客户端,但需要网络中所有服务器的协作。这种新的计算方式将是革命性的,备受吹捧的 Web 服务行业也将因此变得有意义。如果您对此有浓厚兴趣,请告诉我:wytek@szymanski.com

程序集列表

GenericChannels 库

此程序集包含核心客户端和服务器通道以及传输连接和传输接收器的接口定义。除了系统程序集之外,它不引用任何其他内容。

TransportStreams

此程序集包含命名管道和 TCP 套接字的传输连接。它引用 GenericChannels 库。

CustomChannelsLibrary

此程序集包含几个对通用通道的包装器。它引用 GenericChannels 库和 TransportStreams 库。

InterfaceLibrary

这是一个仅包含接口的库。它耦合了客户端和服务器。

TestLibrary

此程序集包含一个用于测试框架的对象。它引用 Interface 库。

客户端

这是一个用于测试的客户端应用程序。它引用 GenericChannels  库、TransportStreams  库和 Interface 库。

服务器

这是一个用于测试的服务器应用程序。它引用 GenericChannels 库、TransportStreams 库和 TestLibrary 库。

© . All rights reserved.