.Net Remoting是微软早前推出的一项分布式通讯技术框架,在.Net架构的程序中有着比较广泛的应用。在WCF中,已经集成了Remoting的技术。不过,他们有着很多相同的概念,如:信道(Channel)、代理(Proxy)、寄宿(host)等。在如今仍有一些分布式系统应用中运行着由Remoting技术构建的系统。本文将描述在服务端与客户端的交互中,他们各自的实现方式。
1、Remoting的实现。
在Remoting中,远程对象是一个重要的概念。服务端通过将它注册到制定的信道中,客户端服务端公布的服务端注册的远程对象的URI,通过代理来使用它。在这种架构下的服务端与客户端要实现相互之间的通讯一般是使用事件的方式进行。
Remoting的通讯模型中分三块,即:1、客户端、2、远程对象、3、寄宿程序(服务端寄宿的应用程序域)。
注:远程对象一般通过继承自MarshalByRefObject,而继承自MarshalByRefObject的对象是不会离开它的应用程序域的。并且为了安全,一般我们都是通过将远程对象实现的接口提供给客户端,而不是远程对象。
一、客户端发送消息到服务端。客户端获取来远程对象的代理,通过使用它的提供的接口以后,通过信道的传输便到了远程对象。远程对象在收到消息后,用事件的方式通过服务端以执行相应的操作。这时,事件是定义在远程对象中的,客户端进行操作以后,服务端可以直接通过订阅远程对象的事件而获取消息。
二、服务端发送消息到客户端。在服务端,由于它是远程对象注册的应用程序域,服务端可以直接使用它。同样,服务端发送消息给客户端,也是通过事件来实现的。与客户端主动发送消息给服务端不同的是,定义在远程对象的事件运行在服务端,无法序列化到客户端。这种情况下,一般我们可以通过一种类似"中介者"的方式来进行操作。
Remoting的程序结构如下图:
说明:
首先,看看远程对象实现的接口定义:
/// <summary> /// 服务端通知客户端时的事件 /// </summary> event DataChangeCallBack DataChangeEventHandler; /// <summary> /// 服务端促触发事件函数 /// </summary> /// <param name="entityName"></param> void ServerSideDataChange(string entityName); /// <summary> /// 客户端调用服务端使用的接口函数 /// </summary> /// <param name="entityName"></param> void ClientSideDataChange(string entityName);
其次:远程对象的定义:
public class RemoteObject : MarshalByRefObject, IDataChange { public event DataChangeCallBack DataChangeEventHandler; public event DataChangeCallBack ClientDataChangeEventHandler; #region IDataChange 成员 public void ServerSideDataChange(string entityName) { if (DataChangeEventHandler == null) return; foreach (Delegate datachange in DataChangeEventHandler.GetInvocationList()) { DataChangeCallBack temp = datachange as DataChangeCallBack; try { temp(entityName); } catch { DataChangeEventHandler -= temp; } } } public void ClientSideDataChange(string entityName) { if (ClientDataChangeEventHandler != null) { ClientDataChangeEventHandler(entityName); } } #endregion public override object InitializeLifetimeService() { return null; } }
注意:在远程对象的显示中,重写来基类的InitializeLifetimeService函数,目的是为了远程对象永不过期。这涉及到远程对象生命周期的问题。有兴趣的同学可以了解一下(为记忆中,远程对象生存期为6分钟)。如果不重写此函数,在远程对象被GC回收后,如果再有客户端想通过URI获取它的代理将会出错。因为它早已被GC回收了。
再次,主要看看服务端通知客户端时,使用的"中介者"对象的定义:
public class EventWrapper : MarshalByRefObject,IDataChange { public event DataChangeCallBack DataChangeEventHandler; #region IDataChange 成员 public void ServerSideDataChange(string entityName) { if (DataChangeEventHandler!=null) { DataChangeEventHandler(entityName); } } public void ClientSideDataChange(string entityName) { } #endregion }
最后就是服务端与客户端信道、事件注册,以及消息的发送问题了。这里主要给出客户端实现(因为涉及服务端事件通知客户端的问题)。服务端类似。
private void ClientForm_Load(object sender, EventArgs e) { UnregisterChannels(); RegisterChannel(); RegisterEvent(); } void UnregisterChannels() { if (ChannelServices.RegisteredChannels.Length == 0) { return; } foreach (IChannel channel in ChannelServices.RegisteredChannels) { ChannelServices.UnregisterChannel(channel); } } void RegisterChannel() { IDictionary hashtable = new Hashtable(); hashtable["port"] = 0; BinaryClientFormatterSinkProvider provider = new BinaryClientFormatterSinkProvider(); TcpChannel tcpChannel = new TcpChannel(hashtable, provider, null); ChannelServices.RegisterChannel(tcpChannel, true); } void RegisterEvent() { instance = Activator.GetObject(typeof(IDataChange), "tcp://127.0.0.1:8888/DataService") as IDataChange; wrapper = new EventWrapper(); wrapper.DataChangeEventHandler+=new DataChangeCallBack(wrapper_DataChangeEventHandler); instance.DataChangeEventHandler += wrapper.ServerSideDataChange; } void wrapper_DataChangeEventHandler(string entityName) { if (txtReceiveMsg.InvokeRequired) { txtReceiveMsg.Invoke(new ShowMessageCallBack(wrapper_DataChangeEventHandler), entityName); } else { txtReceiveMsg.Text = entityName; } } private void btnSend_Click(object sender, EventArgs e) { if (!string.IsNullOrEmpty(txtSendMsg.Text)) { instance.ClientSideDataChange(txtSendMsg.Text); } }
程序运行界面如下图:
服务端:
客户端:
注:在信道的定义中,应该使用与之对应的格式化标识符。
2、WCF的实现
在WCF中,实现服务端与客户端的通讯一般通过双工的模式进行。在WCF中支持双工的绑定协议有:netTcpBinding、wsDualHttpBingding。在WCF实现的服务端与客户端通讯的示例中,为选择来前一种协议。
程序的结构图如下:
同样,为了可视化,Host与Client也为Winform程序。
在WCF的实现中,实现的功能为Remoting完全相同。
首先看看服务契约定义:
[ServiceContract(CallbackContract=typeof(ICallBack))] public interface IDataChange { [OperationContract(IsOneWay=true)] void Register(); [OperationContract(IsOneWay=true)] void ServerSideDataChange(string entityName); [OperationContract(IsOneWay = true)] void ClientSideDataChange(string entityName); }
回调契约定义:
public interface ICallBack { [OperationContract(IsOneWay=true)] void DataChange(string entityName); }
服务的实现代码:
[ServiceBehavior(InstanceContextMode=InstanceContextMode.Single,ConcurrencyMode=ConcurrencyMode.Single)] public class DataService : IDataChange { public event DataChangeCallBack ReceiveClientDataChangeHandler; ICallBack callBack; #region IDataChange 成员 public void Register() { callBack = OperationContext.Current.GetCallbackChannel<ICallBack>(); } public void ServerSideDataChange(string entityName) { callBack.DataChange(entityName); } public void ClientSideDataChange(string entityName) { if (ReceiveClientDataChangeHandler != null) { ReceiveClientDataChangeHandler(entityName); } } #endregion }
服务在Winform中的寄宿代码如下:
private void ServerFrom_Shown(object sender, EventArgs e) { Thread thread = new Thread(ServiceStart); thread.Start(); } void ServiceStart() { Uri address = new Uri("net.tcp://127.0.0.1:8806/DataService"); using (ServiceHost host = new ServiceHost(service, address)) { host.Open(); tipStatus.Text = "服务已经启动"; while (true) { Thread.Sleep(100); } } }
而在客户端,直接通过窗体实现了回调接口,将窗体封装在InstanceContext里,让窗体对象成为服务端回调操作的对象,服务端回调客户端时直接将信息发送给窗体。在客户端窗体的FormLoad事件里做如下定义:
InstanceContext context = new InstanceContext(this); dataChange = new ClientProxy(context); dataChange.Register();
客户端通知服务端的方式比较简单,也就是主动去调用服务接口。代码如下:
if (!string.IsNullOrEmpty(txtSendMsg.Text)) { dataChange.ClientSideDataChange(txtSendMsg.Text); }
这样,在客户端调用服务端接口时候,触发服务端事件来通知窗体做数据显示。
服务通知客户端时通过回调客户端,将消息发送给客户端。代码如下:
Form.cs
if (!string.IsNullOrEmpty(txtSendMsg.Text))
{
service.ServerSideDataChange(txtSendMsg.Text);
}
//DataSerive.cs
public void ServerSideDataChange(string entityName)
{
callBack.DataChange(entityName);
}
如此这般,便实现了在WCF架构下实现服务端与客户端的通讯。
运行界面与上面类似。就不重复给出了。