.NET Remoting的广播和回调问题
似乎很多人都用.NET Remoting来做有广播和回调(包括事件)的Client-Server程序,虽然对这种不正常用法不太喜欢,但最近也切身趟了一次这滩浑水。注:阅读本文需要对Remoting有一定的基本了解。
说这种做法不正常,是因为.NET Remoting在很大程度上是针对Web Service设计的。也就是说,它的通信模式是Web的Client端主动请求模式,Server端对每次请求的处理都是独立的,也不会主动去访问Client端。这就是说,Server向Client主动发信息,不管是广播还是事件通知,都是非标准用法,并且缺省安全配置下是禁止的。也就是说,.NET Remoting不是用来做这个的。
那为什么还要用它做这个活呢?因为.NET Remoting的用途不只是网络上的Client-Server通信,还用于跨越AppDomain的本机通信,尤其是进程间通信。另外在局域网内,使用广播和回调也不担心安全问题。而且使用.NET Remoting的IpcChannel是唯一可以使用NT的命名管道通信的现成API,否则就得调用Win32 API。
因为缺省的安全配置不允许,所以要将安全级别改成最低,需要在创建Channel时指定Server Sink。这里以Binary Formatter和IpcChannel为例:
BinaryClientFormatterSinkProvider clientSink = new BinaryClientFormatterSinkProvider();
BinaryServerFormatterSinkProvider serverSink = new BinaryServerFormatterSinkProvider();
serverSink.TypeFilterLevel = TypeFilterLevel.Full;
IDictionary properties = new Hashtable();
properties["portName"] = "localhost:9090";
IpcChannel channel = new IpcChannel(properties, clientSink, serverSink);
ChannelServices.RegisterChannel(channel, false);
RemotingConfiguration.RegisterWellKnownServiceType(
typeof(MyServer), "MyServer", WellKnownObjectMode.SingleCall);
要注意的是Server主动通知Client的本质是Client自己也创建了个Server,然后Server和Client的位置颠倒,由Server主动去向Client发请求。所以,如果在同一台主机上,Client和Server的端口号不能一样。对命名管道而言,其实是命名管道的名字不能一样。Client端样例:
IpcChannel channel = new IpcChannel("localhost:9091");
ChannelServices.RegisterChannel(channel, false);
MyServer server = (MyServer)Activator.GetObject(typeof(MyServer), "ipc://localhost:9090/MyServer");
那么然后是如何定义MyServer的接口了。在MyServer上定义event不是一个好办法,因为.NET Remoting机制要求Server必须知道响应事件的对象的类型,即Server必须引用Client的Assembly。这使得Client完全无法独立扩展,而且Server和Client必须一起编译以保证交互接口完全一致。一个较好的办法是使用一个interface,Client端定义一个实现该interface的类,并将对象的引用传给Server,Server就可以通过该interface调用Client端的函数了。这样,Client和Server都只需要引用定义该interface的Assembly,而不需要知道具体的实现类型。当然,实现该interface的对象必须是可用于Remoting的,即MarshalByRef或Serializable。Server端如下:
public class MyServer : MarshalByRefObject
{
public void AddCallback(ICallback callback) { ... }
}
public interface ICallback
{
void Notify();
}
Client端如下:
class MyClient : ICallback
{
void ICallback.Notify() { ... }
}
另外注意不要在MyServer里用成员变量保存数据和对象引用,因为Server的接口对象的作用只是接口,随时可能被丢弃或重新创建,即使用Singleton模式也是一样,即使使用Object Life Time Lease Management显示指定也不能保证,谁也不能保证Server不当机、网络不断线。除非是可以不在乎出错的情况,否则严格遵循每次对Server的请求都是独立的这一条约定。必须要保存的数据可以交给Singleton的对象或数据库。
更新:以上说的是WellKnownServiceType的情况,如果使用Client激活模式则是另一种用法。