XML Web Services原先的一个问题是不能实现真正的Callback。比如用ASP.NET实现的时候,每一个[WebMethod]都是一个远程方法调用,但只支持方法效用而不支持事件(Event),不能像本地调用可以传一个Delegate来实现Callback(Callback、函数指针、Listener模式、中断等其实都是一回事,都是一种事件响应)。
Web Services里面不能支持事件是很不方便的,很多应用就受限制,或者因此就放弃了Web Services技术。当然,也有一些Workaround,比如可以轮询——Outlook Web Access就是轮询的,所以能做到有email来就在屏幕右下角出一个小窗口,效果和MSN Messenger一样,这很酷;或者也可以在客户端起一个Remoting的服务器,把Remote Object的URI传给Web Services,等事件来了以后服务器再去Call客户端,这当然也是也可以。
不过这些Workaround要么有些缺点(性能问题),要么不够直接(Remoting太“重”了)。这就好像你可以在JavaScript里面实现全部的OO,但这会非常非常繁琐。这又好像通过Attribute可以在.NET里面实现AOP,但总感觉不直接。
WSE 2.0 (Web Services Enhancements 2.0)提供了一些TCP Messaging的功能,很好地解决了这个问题(在Web Services架构里面实现原生的Callback)。我写了一个简单的例子来演示Web Services如何支持事件回调:
1. 服务器端的主要代码(片断):
using Microsoft.Web.Services;
using Microsoft.Web.Services.Messaging;
using Microsoft.Web.Services.Addressing;
private ArrayList Listeners
{
get
{
return (ArrayList)Application["Listeners"];
}
}
[WebMethod]
public void AddListener(string listener)
{
this.Listeners.Add(listener);
}
[WebMethod]
public void RemoveListener(string listener)
{
for(int i=0;i<this.Listeners.Count;i++)
{
if(((string)this.Listeners[i]).Equals(listener))
{
this.Listeners.RemoveAt( i );
return;
}
}
}
[WebMethod]
public void FireEvent()
{
for(int i=0;i<this.Listeners.Count;i++)
{
SoapEnvelope envelope = new SoapEnvelope();
envelope.SetBodyObject("blah blah");
envelope.Context.Action = new Action((string)(this.Listeners[i]));
envelope.Context.ReplyTo = new ReplyTo(new System.Uri((string)(this.Listeners[i])));
SoapSender peerProxy = new SoapSender(new System.Uri((string)(this.Listeners[i])));
try
{
peerProxy.Send(envelope);
}
catch(Exception e)
{
this.Listeners.RemoveAt( i );
}
}
}
[WebMethod]
public string GetListeners()
{
string listeners="";
foreach(string item in this.Listeners)
{
listeners+=item+";";
}
return listeners;
}
一些说明:
a) Application["Listeners"]是在Application_Start里面创建的;
b) Microsoft.Web.Services.*就是WSE的Namespace,需要到微软网站下载安装了才会有;
c) AddListener、RemoveListener、FireEvent都是很好懂的代码,典型的Subscriber模式,其中主要就是靠SoapSender.Send()来最终完成回调,call客户端的事件处理代码。
2. 客户端的主要代码(片断):
public class Form1 : System.Windows.Forms.Form
{
private void buttonAdd_Click(object sender, System.EventArgs e)
{
WSEClient.sha_zheng_0a.Service1 server=new sha_zheng_0a.Service1();
server.AddListener(this.myUri.ToString());
this.textBoxOutput.Text+="\r\n\r\n"+server.GetListeners();
}
private void buttonRemove_Click(object sender, System.EventArgs e)
{
WSEClient.sha_zheng_0a.Service1 server=new sha_zheng_0a.Service1();
server.RemoveListener(this.myUri.ToString());
this.textBoxOutput.Text+="\r\n\r\n"+server.GetListeners();
}
private void buttonFire_Click(object sender, System.EventArgs e)
{
WSEClient.sha_zheng_0a.Service1 server=new sha_zheng_0a.Service1();
server.FireEvent();
}
private void buttonList_Click(object sender, System.EventArgs e)
{
WSEClient.sha_zheng_0a.Service1 server=new sha_zheng_0a.Service1();
this.textBoxOutput.Text+="\r\n\r\n"+server.GetListeners();
}
private void Form1_Load(object sender, System.EventArgs e)
{
int port=(new System.Random()).Next(3000,7000);
this.Text="Client Port: "+ port;
MyReceiver.mainform=this;
myUri = new Uri("soap.tcp://" + System
SoapReceivers.Add(myUri, typeof(MyReceiver));
}
System.Uri myUri;
}
public class MyReceiver : SoapReceiver
{
protected override void Receive(SoapEnvelope envelope)
{
mainform.textBoxOutput.Text+="\r\n\r\nEvent Fired";
}
public static Form1 mainform;
}
一些说明:
a) WSEClient.sha_zheng_0a.Service1就是Web Services客户端的Proxy;
b) MyReceiver类继承了SoapReceiver类,并重载了Receive()函数,这就是客户端的Callback函数了;
c) 在此之前,先要用SoapReceivers.Add()把MyReceiver类型作为一个接收器绑定到一个端口上进行侦听,然后,把完整的URI注册到服务器端(相当于订阅这个事件)。
3. 效果:
同时可以运行三份客户端程序(分别绑定在不同端口上)并注册到服务器。然后,只要服务器端有事件被触发(FireEvent),所有注册的客户端都能做出响应(在输出框中打印“Event Fired”)。
下面是一个截屏:

4. 好在哪里:
客户端很“轻”,而且这套事件响应机制已经是Web Services的一部分,是原生支持的。
“轻”,原生支持,这是最好的两点。
其实,很多技术之间并没有很明显的革命性提高。往往就是变得方便了一些、简洁了一些、容易了一些、快了一些,就已经是很大的提高了。
打印 | 张贴于 2004-03-08 21:56:00 | Tag:Dot NET

留言反馈
public delegate void MessageRecievedHandler(SoapEnvelope envelope);
楼主的写法没有考虑跨线程调用,我在WSE3.0中调试似乎有些问题。
public class MyReceiver : SoapReceiver
{
public event MessageRecievedHandler MessageRecieved;
protected override void Receive(SoapEnvelope envelope)
{
OnMessageRecieved(envelope);
}
private void OnMessageRecieved(SoapEnvelope envelope)
{
if (MessageRecieved != null)
MessageRecieved(envelope);
}
}
private void Form1_Load(object sender, EventArgs e)
{
int port = (new System.Random()).Next(3000, 7000);
this.Text = "Client Port: " + port;
myUri = new Uri("soap.tcp://" + System.Net.Dns.GetHostName() + ":" + port + "/MyEventListner");
MyReceiver receiver = new MyReceiver();
receiver.MessageRecieved += new MessageRecievedHandler(receiver_MessageRecieved);
SoapReceivers.Add(myUri, receiver);
}
delegate void Update(TextBox textBox, string message);
public void UpdateText(TextBox textBox, string message)
{
textBox.Text += "\r\nEvent Recieved";
}
void receiver_MessageRecieved(SoapEnvelope envelope)
{
if (this.textBoxOutput.InvokeRequired)
this.textBoxOutput.Invoke(new Update(UpdateText), this.textBoxOutput, "\r\nEvent Recieved");
}
但楼主有个小问题:
[WebMethod]
public void FireEvent()
{
for(int i=0;i<this.Listeners.Count;)
{
SoapEnvelope envelope = new SoapEnvelope();
envelope.SetBodyObject("blah blah");
envelope.Context.Action = new Action((string)(this.Listeners[i]));
envelope.Context.ReplyTo = new ReplyTo(new System.Uri((string)(this.Listeners[i])));
SoapSender peerProxy = new SoapSender(new System.Uri((string)(this.Listeners[i])));
try
{
peerProxy.Send(envelope);
i++; // 因为楼主在循环中修改了Listeners。
}
catch(Exception e)
{
this.Listeners.RemoveAt( i );
}
}
}
我认为您的方案还是是轮询的办法好。
能不能告诉我,remoting 怎么取客户端ip?
非要 客户端写代码传递吗?
1,我水平低,忘了在Application_Start作案。
2,那两句代码改为:
envelope.Context.Addressing.Action = new Action((string)(this.Listeners[i]));
envelope.Context.Addressing.ReplyTo = new ReplyTo(new System.Uri((string)(this.Listeners[i])));
就可以了。。。:)
,
我估计你用的是WSE 2的正式版本。
-
的确可能有不一样的。毕竟.net 1.0 beta和rtm也有不少的名字区别的
我用wse2临摹了一下,名称空间好像是Microsoft.Web.Services2;
这也不打紧,可要命的是
envelope.Context.Action = new Action((string)(this.Listeners[i]));
envelope.Context.ReplyTo = new ReplyTo(new System.Uri((string)(this.Listeners[i])));
这两句编译错误啊,看wse2的帮助说现在的Context好像不支持这样了,我改成了:
ResponseSoapContext.Current.Addressing.Action = new Action((string)(this.Listeners[i])); ResponseSoapContext.Current.Addressing.ReplyTo = new ReplyTo(new System.Uri((string)(this.Listeners[i])));
这样就通过编译了,可是连调用AddListener方法都说错,好像是说那个ArrayList 还是空的啊 ,头晕阿,老大!
问问WSE2。0到具体哪里DOWN?
这个嘛,本来就是web services的“增强”——enhancements。:D 当然已经不能算纯粹的"web" services啦
WS-Messaging还可以通过smtp来传呢...研究中
btw,现在好像很少人玩星际了....
我以前至少用6个农民才造狗窝的...
我觉得这种用法似乎已经不能算"WEB" Service了,呵呵
原来只是用了张Longhorn的皮(比起XP的桌面,这一张显得很“丰收”啊)~~~ :D
其实现在我对Longhorn(也就是它提供的所谓WinFX类库)的态度很像当年对待.NET Framework,就是只看文档和相关资料,不花过多时间实践(尤其是对未release的产品)。对于有些基础的朋友,这样可能算是一种捷径吧?嘿嘿~~~不过内心中还是很希望Longhorn能够早点RTM,因为像你所说的那些“能很方便的解决一个令人困惑很久的很具体的小问题的新东西”在这个平台中实在太多了!相信它也将给我们带来的更多的激情和机遇。 :)
但这个实现的好处有:
- 代码比较简洁易懂
- 比remoting“轻”
- 对SoapEnvelop等内建支持
- wse是一个语言无关的协议,例如服务器可以用Java来实现;而remoting只能用.net
这些是我的体会。
这点我想到了,但是忘记写上去了。
这个例子肯定是不能在防火墙、HTTP Proxy或者NAT后面运行的。没办法。
好在这在企业内部的业务整合方面肯定是可以用的。
WSE 2.0里面好多东西,要有心理准备哦。
不过我的体会,WSE 2.0主要就是两块,一块是和安全、身份等有关的,一块是对soap消息传递增强的(上面的例子就是这一块的)。
要是一个新东西能很方便的解决一个困惑我很久的很具体的小问题,我就马上会被打动了。
有机会还要先看看infopath和smart document。新东西真多啊,光微软的就来不及研究。还有alphaworks和developerwork上的东西,等等。
看看blog是个不错的提高途径。
我还没碰过longhorn呢。我们那里一个同事装了,结果发现explorer.exe有内存不断迅速增大的现象 :D 于是我决定再观望一下。
我就是在win xp下搞的,满容易的。
WSE 2.0,有空得好好看看:)
mvm,来参加周五Grace组织的聚会吧
BTW: “其实,很多技术之间并没有很明显的革命性提高。往往就是变得方便了一些、简洁了一些、容易了一些、快了一些,就已经是很大的提高了。”——Truly agreed!