Ajax的长连接,或者有些人所说的Comet,就是指以XMLHttpRequest的方式连接服务器,连接后服务器并非即时写入相应并返回。服务器会保持连接并等待一个需要通知客户端的事件,该事件发生后马上将数据写入响应,这时候客户端就以相当“实时”的方式接收到事件通知。具体的通信模型,请参考这篇文章:《Comet:基于 HTTP 长连接的“服务器推”技术》,里面已经说得非常详细了,我就不再复述了。
我们接着开始讨论如何使用.NET实现这个模型。首先我们能想到的是,我们需要一个Web Service,可以是ASP.NET Web Service,也可以是WCF Web Service,ASP.NET AJAX Library两者都支持。在这里,为了简单起见,就选择大家更熟悉的ASP.NET Web Service举例。然后,我们写下以下两个函数签名:
public void Send(Message message);
public Message Wait();
其中,Send函数用来发送一个Message对象,而Wait函数用来等待一个Message对象。然后,让我们来讨论一些细节问题。
无事件导致超时
首先,长期保持连接时不行的。对于服务器和客户端来说,这不是个问题,但我们永远都要记住中间可能存在各式各样配置怪异的网关和代理,它们上面可能有各式各样的超时规则,因此Comet最好设计为定期重连。一般情况下,如果30秒没有任何事件发生,服务器端就应该通知客户端确实没有事件发生,结束掉本次请求,然后重新开始一次新的请求以便继续等待。
那么上述函数签名可否用来返回一个无事件的消息呢?这是显然可以的,我们可以选择返回null表示无事件,或者返回一个EmptyMessage常量,这视乎我们使用class还是struct来定义Message。(甚至,我们还可以做一个名为NoMessageMessage的Message派生类来做这个事情。)
定义发送目标
上述函数签名确实能用来收发消息,但是没指名发给谁。可能有人会说,发送给谁可以在Message类里面通过一个属性来定义啊。但是Wait()方法没有说明接受方是谁,服务器端依然不知道哪些消息应该让你接收。
因此,我们引入Channel的概念,Channel使用其名称来标识,相同名称的就必然是同一个Channel。在发送与接受时,通过名称指定要发送到哪个Channel,这样问题就解决了。此时,函数签名修改如下:
public void Send(string channelName, Message message);
public Message Wait(string channelName);
可靠的消息队列
想象一个可能发生的情况,服务器端向你发送一个消息,你没有成功接收,但是服务器端认为发送了就成功了,消息从队列删除了,然后这个消息就永久丢失掉了。可能有人会强调TCP多么可靠,服务器端发送的消息如果在TCP的层面发生问题了,肯定会引发Socket级别的Exception,这个Exception冒泡上来,服务器端就能截获,从而得知发送失败,然后先不删除队首消息。可是别忘了,中间是可能存在代理的,如果代理成功把消息收回去了,可是代理发送到客户端这一步失败了,服务器端就不一定会发生异常了。
因此,我们需要制定一种策略,来确保下行消息总能发送到客户端。在这里,我们选择了引入逐个ACK的机制,来确认消息的接收。也就是说,服务器端发送给客户端的消息带有一个序号,在客户端收到消息后就将该序号发回给服务器端,已确认它受到了该消息。这时候,函数签名更改如下:
public int Send(string channelName, Message message);
public Message Wait(string channelName, int sequence);
我们使用Wait()接收到的Message中,应该有一个Sequence的属性,标记它的序号。然后,再我们执行下一次Wait()时就将该序号加1的值通过sequence参数传递回去,让服务器知道我们期望下一条消息的编号是这个。例如我们收到Message,其Sequence属性为836,那么下一次调用Wait()的时候就传给服务器837。服务器端此时应该保留了编号为836的Message在对首,如果客户端继续请求836号消息,证明它上次没收到,这次仍然发送836号消息给它;如果客户端请求837号消息,证明它成功收到836号消息的,这次就发送837号消息给它。
如果都不是,那该怎么办?那意味着,这是一个错误的请求,甚至可能是攻击请求,因为正常情况下不应该出现这样的请求的,服务器端可以考虑抛个无关紧要的Exception(不要告诉攻击者你知道他在攻击了),甚至直接给个400 (bad request)的响应代号。
与Wait()类似的,Send()也可以加入ACK机制,只需要将返回类型从void改为int就可以了,这个值就专门用于传递消息编号,实现方式和Wait()是一样的,不过Send()是由客户端保存待发送消息的队列。
小结
到此为止。我们的Web Service就写好了。这就写好了?只有签名没有函数体?是的,复杂的工作留给model去做,Web Service在这里只是相当于一个view,用于将model的接口暴露出来。
在下一次的文章中,我们将开始讨论如何实现服务器端的消息传递机制。如果你对本文章系列感兴趣的话,欢迎订阅我的blog: