Web聊天室

 

前言
本文阐述了如何在微软体系下实现基于推(Push)的Web聊天室。并详细讲述了如何在这

种情况下减轻服务器端的负担。这里并不介绍过多的编程技巧,它们不是本文的重点,

本文的目的主要是介绍整个聊天室的组织结构以及其服务器端的内部结构。

之所以详细介绍服务器端,是因为对于任何一个Web应用程序,尤其是想聊天室这样高消

耗的Web程序,服务器端的性能是非常重要的。相对于客户端,服务器端受到软件设计质

量和代码编写质量的影响更大。在代码效率较低的情况下,如果放在客户端,其影响只

是一个常数;而在服务器端,随着客户数目的增加,其对服务器的不利影响将呈线性增

长,当到达一定程度时更将变为指数增长。

因而,仔细设计编写服务器端是非常重要的,这也是本文的目的之一。至于客户端,主

要讲述了如何与服务器端对接,并简述了用户界面技巧和提交请求的技巧,更详细的设

计应属于网页制作范畴,非本文重点。

本文中服务器端使用Windows 2000操作系统,自带的IIS 5.0 Web服务软件;客户端使用

IE 5.0以上版本。服务器端使用Visual C++ 6.0开发,使用了ISAPI和COM技术,与ASP结

合来处理用户请求。客户端使用JavaScript编写脚本。

现在,Web上有许多的聊天室,它们使用户可以通过浏览器进行聊天,而不必下载专用的

客户端。Web聊天室的优点也在于此,它适用范围广,几乎所有接入Internet的用户都装

有浏览器。下载专用客户端可能是用户比较反感的,因为相对于使用浏览器来说,它们

比较繁琐。更重要的是,使用浏览器,能使安全性得到更好的控制,在浏览器中的程序

是严格受限制的,它们不会(或很难)做出对用户不利的举动。

现有的Web聊天室从获取信息的方式上,大概可以分为两类,一类是基于刷新的,另一类

是基于“推”(Push)的。

基于刷新的聊天室的原理是,在客户端的网页中使用某种机制,使该页面每隔一定时间

自动刷新一次,籍刷新所发出的HTTP请求,从服务器获取最新的发言信息。可用的定时

刷新机制有HTML的Meta标签和脚本的setInterval()方法,在此不详细讨论。这类聊天室

的优点是实现简单,而且,如果网络延迟不是问题的话,其总体成本将是最优的。但实

际上,网络速度经常是一个很重要的问题,而由于这种聊天室网络传输量大,在一个低

速的网络上,延迟将变得很明显。再有,尽管它能够定时刷新,用户仍然不能即时的看

到在两次刷新之间的新发言。频繁的数据传输任务还将加重服务器的负担。

既然解决问题的关键是消除对旧数据的重复传输,那么最好的方法就是只在新信息出现

时才开始传输。而唯一知道所有新信息何时出现的只有服务器。因次,答案是需要一种

服务器主动的传输机制。目前,Web上的数据传输都是以“拉”(pull)的方式进行的,

即,客户提出请求,服务器响应请求,在这个过程中,客户端是主动的,而服务器端是

被动的。基于刷新的聊天室正是沿袭了这种机制。而现在需要的是一种被称为“推”的

方式,当有新信息到达时,我们希望服务器端能够直接将信息推到客户端。但是很明显

,由于现有的协议的限制,服务器端不可能直接发起连接到客户端并传输数据,整个实

现空间被完全限制在现有的拉的方式中。

为了在现有的拉环境下实现推,必须将拉的方式变通一下,将推嵌入到拉中,而将一个

拉的过程扩展到整个聊天过程的长度。由客户端以通常拉的方式发起一个连接到服务器

端,然后,该连接一直保持,当有新信息到达时,服务器便将该信息经由用户打开的连

接送出。从客户端来看,该过程就好像一个长时间的拉过程,服务器的响应是断续的,

每次响应都是一条(或多条)新信息。可见,所有的推都是基于用户发起的一个连接,

这里称其为“ 推信道”,该信道由用户“打通”。

实际测试证明,Web请求具有很好的持续性,在服务器没有任何响应(没有新信息)的情

况下,推信道可以维持很长的时间,至少可以维持5分钟。这已经足够了,实际上服务器

和客户不应该有长达5分钟的时间没有交互,原因将在后文阐述。

基于推的聊天室的优点是很明显的,它既减节约了网络带宽、降低了网络延迟,又减少

了服务器的负担。但是也有其缺点,那就是实现难度大,如果处理不好,整体效果可能

会不及基于刷新的聊天室。再有,基于刷新的聊天室开发较复杂,因而造成其开发成本

较高,如果网络带宽和服务器负担实际上不是问题,其总体拥有成本将高于基于刷新的

聊天室。

客户端

推的原理已经确定了,现在来看具体实现。相对于服务器端的实现,客户端的实现可能

更棘手。因为客户端不像服务器端——怎么做都行,在服务器端的开发自由度是相当大

的,虽然有点夸张,但是只要你愿意,使用核心代码也未尝不可;而客户端却截然不同

。在此,实现的空间又被限制在了一个狭小的范围内,实现中最好只使用HTML,这在基

于刷新的聊天室中可以实现,但是在基于推的聊天室中几乎不可能。为了实现它,就必

须放宽对客户端实现空间的限制,有必要使用活动脚本,或者说使用IE的JScript,结合

其DOM(Document Object Model),来实现一个嵌入IE的“客户端”。

在IE中,一个页面可以是“未完成”(pending)的,当一个页面的URL提交后,随着服

务器端数据的不断到达,该页面的状态(readyState属性)一直为“interactive” ,

即可以交互的。此时其DOM已经初始化,可以访问了。该页面使用的连接即可作为推信道

,信息可以被不断的推到该页面上。

但遗憾的是IE并没有提供通知未完成页面内数据到达的事件,可能与此有关的三个事件

:“onreadystatechange”、“ondataavailable”和“onpropertychange”在这里都不

适用或不可用。因而,不得不设置一个定时器,周期的来检测是否有新数据到达。与基

于刷新的聊天室所使用的定时刷新机制所不同的是,这里的检测是完全发生在客户端的

,它检测的是本地的页面;而基于刷新的聊天室则是检测服务器。因此,这里的定时检

测不会加重网络和服务器的负担。定时器的精度将影响到用户界面上显示的延迟,这里

将间隔设置为0.5秒,该间隔造成的延迟是可以接受的,而且其不会对客户端处理器造成

负担。

DOM是用来管理文档的好方法,为了便于客户端检测新信息时利用DOM,将服务器推来的

每条消息嵌到一对HTML标记中(这里使用“<p></p>”标记,原因是其占的空间少)。依

据微软文档,使用DOM,可以从一指定的根处取得针对某一标记的集合,该集合中每个元

素为一个该类型的标记,集合有指明集合内元素数目的属性——“length”。每个元素

都可由其在该集合中的索引定位,而且索引是按照该元素对应的标记其在HTML文档中的

位置递增分配的,间隔为1。

客户端总是记录着上一次检索时标记<p>的数量,当一次定时器嘀哒到来时,只要检测当

时标记<p>的数量,与之前记录的数量相对照,就可以判断是否有新消息到达。又由于索

引是递增分配的,新的信息的索引便可以预测,依据其索引便可以将其标记从集合中取

出,对每个(可能在两次定时间隔中到达多条信息)取出的标记,都可以通过其innerHT

ML属性取出消息内容,然后送脚本分析。

既然推信道实质上是一个TCP连接,就不能保证其不会中断,必须设法应付推信道的意外

中断,否则,一旦中断发生,用户将接收不到任何信息。处理意外中断的方法很简单,

一旦连接中断,页面的readyState属性将变为complete,只要检测该属性的值,再根据

客户端当前的实际状态,便可判断是否发生意外中断。一旦中断发生,客户端脚本应自

动重新提交页面,以便再次打通推信道。此时也应复位客户端标记计数器。

对每条送分析的消息,根据其内容,由脚本进行相应的操作,与用户进行交互,如显示

一条发言内容,或者因为一个用户退出聊天室而从成员列表中删除该用户。因而整个用

户界面的主体都是由脚本根据当前状态动态生成的。要使用脚本生成页面内容,仍然要

使用DOM,使用内置document对象的createElement方法,以及各个标记的appendChild方

法、insertBefore方法和removeChild方法来操作DOM树,操作结果将在用户界面上实时

的表现出来。之所以不使用innerHTML属性直接插入标记,是因为其性能相当低下,有显

著的延迟,而且不如DOM方便。

脚本负责的另一个工作就是与服务器进行交互,用户的发言、进入和退出等动作都要籍

由脚本来代理操作,因为用户不可能(或者不方便)与服务器直接交互。与服务器交互

的方法很多,最简单的是使用表单(form)。但是,提交表单会刷新其所在页面,而基

于推的聊天室是不需要也不应该刷新的。如果要使用表单,就必须内嵌一个或多个IFRAM

E,在其内部载入另外的包含所需表单的页面,使用脚本将用户的输入从主页面拷贝到这

些表单中,然后通过调用表单的submit方法提交它们。这时就又遇到了与实现推时类似

的问题,主页面无法得知其提交的页面是否已经返回,这时又需要定时检测,这会增加

实现的复杂度,同时会带来更多的不稳定因素。另一种可行的方法是在每个返回的页面

中都嵌入脚本,挂接该页面的onload事件,然后通过跨框架调用的方法通知主页面该页

载入完成。但这些方法都不够理想。最佳的方法还是使用IE5新支持的XMLHTTP组件。该

组件的ProgID为“Microsoft.XMLHTTP”,正如其名,它主要操作XML数据。重要的是它

支持异步操作和回调函数,使用它便可以通过纯脚本的方式与服务器进行交互。美中不

足的是,在回调函数的上下文中没有对其XMLHTTP对象的引用,必须使用其它方法(例如

全局变量)才能获得该对象。如果微软能让回调函数带一个引用其所挂接XMLHTTP对象的

参数,或者让this变量引用该对象就更好了。

有关使用脚本操作UI与用户交互,以及使用脚本与服务器交互的详细内容,不是本文的

重点,这里不再赘述。

服务器端在服务器端,最重要的是实现一个符合上述标准的推机制。这个机制最显著的

特征就是异步性,它经常,或者说绝大多数时间是不工作的,只在有新信息到达时才变

为活动的,并为用户发送这些新信息。

Windows是一个对异步操作支持得很好的操作系统,针对上述要求,很自然的让人想到使

用多线程,每个线程负责一个用户连接,其大部分时间是挂起的,当新信息到达时,它

被以某种机制唤醒,然后为用户推数据。因而,看起来,只要写一个使用多线程的服务

器端处理用户请求即可。但实际上,客户端并不只是接收服务器推来的信息,它们还要

与服务器进行交互,例如向服务器发送用户的发言内容。很明显,处理这些交互的最简

单的途径是使用ASP,ASP脚本功能可以方便的分析和处理用户请求,组织和生成回复内

容,而且具有很好的弹性,维护非常方便。但是,ASP却不具备完成服务器推所需的能力

,它在设计时就不是用来处理大量得异步操作的。为了能鱼和熊掌兼得,有必要将二者

结合起来,在所有让ASP与二进制代码能够互访的机制中,最好的就是微软的COM模型,A

SP完全支持COM,只要让二进制代码实现COM,便可以与ASP进行良好的交互。

既然选定COM,就要选定可执行文件模式和线程模型,为性能考虑,最好的方式是将组件

做成DLL,让其加载到IIS进程(实际上是IIS的子进程,DLLHOST.EXE)的地址空间中直

接调用,这样就省去了跨进程边界所需的marshaling,因而提高了性能。既然使用DLL,

则实现推的部分也必然要做到该DLL中,否则,如果实现推的部分运行在其它进程中,则

该DLL无异于一个proxy,进程间通讯仍然无法避免,DLL形式的组件所带来的性能提升将

被抵消。由于推部分已经结合到组件中,而推部分的实现必须是多线程的,就是说整个

程序使用多线程已经是无可避免的,更加上性能的考虑,组件无疑应该选用多线程模型

,即Free或Both。现在,按照设计,实现推的代码将在IIS的进程执行,要在IIS进程内

运行的代码中实现推,方法有很多,但最优秀的方法莫过于使用IIS的ISAPI接口。使用

该接口,应用程序直接与IIS进行交互,而不是客户端,IIS负责线程和客户端连接的管

理,同时负责信息的发送和接收以及一些分析工作。这样可以显著的减少代码量,降低

开发复杂度,更能与Web服务器紧密结合,提高整体性能。

ISAPI部分指ISAPI扩展(Extension),作为推的实现部分,可以使用普通请求.dll的方

式来请求它的服务。也可以将其配置为脚本引擎,将某个文件扩展名映射到该引擎上,

让它控制一个子区域,用户只要请求一个该区域下具有该扩展名的任意名称文件即可获

得服务。

这样,该DLL中实际上是两个部件,一个作为与ASP交互的组件出现,另一个作为与IIS交

互的ISAPI扩展出现。从IIS的角度看,两部分是分别被加载和初始化的——各自都是在

第一次被调用时,只不过加载的是同一个DLL。因为可能出现虽然在同一DLL中,但一部

分已经在工作,而另一部分还未初始化的情况,在设计时应考虑到这两部分的同步问题

接下来设计服务器端结构,服务器端设计的重点是使服务器端的性能达到最高。其中最

关键的是合理的使用多线程技术。另外,尽量使用操作系统已有的功能,以简化程序实

现。例如这里选用的操作系统是Windows 2000,Windows 2000已经内置了哈希表的实现

,所以,在使用大量字符串时就没必要再编写自己的哈希表。

聊天室组件应该能够支持多个聊天室,而不是单个,以便对聊天内容分门别类。因而要

求有一个“大厅”,用户刚登录聊天室时,处于大厅中,在此可以检视各个聊天室状态

,决定进入哪个聊天室。为了管理每个开放的聊天室,需要一个类来描述那些开放的聊

天室,这里称之为聊天室描述符。同样,为了管理每个在线的用户,也需要一种称之为

用户描述符的类来描述它们。最后,为了管理这些聊天室和用户描述符的多个实例,需

要一个总目录来对它们进行索引。

由于聊天室描述符和用户描述符都是多实例的,尤其是用户描述符,在运行过程中要频

繁的分配和释放,极易产生内存碎片,因而有必要对它们使用特殊的管理方式。通过对

这些类使用其自己的私有堆,便可以解决碎片问题。

考虑,当用户登录后,就立即发起一个到服务器的连接,该连接请求组件的ISAPI部分,

ISAPI部分验证用户身份后找到用户的描述符,然后将该请求的ECB(Extension Control

Block)作为推信道挂接到描述符中。最后函数向IIS返回PENDING,表示处理未完成。

当最终用户发言时,发言内容被传输到服务器,IIS将请求交给ASP来处理,ASP验证用户

身份、对请求做一些分析和处理后,通过COM组件的界面,调用相应的方法,通知组件某

个用户发言。组件检查该用户所处的位置,对其所在的聊天室内的每个用户都发送该发

言内容,通过调用IIS的ISAPI接口,发言内容经由每个用户事先建立的连接作为回应发

送出去——这就是整个“推”的过程。

对于发送信息的过程,如果按照前面的设想,每个用户占用一个线程,固然可以使用一

次WriteClient()调用来完成发送。但是,尽管一个线程相对于进程所消耗的资源是相当

少的,但如果用户数目很多,服务器就要维护相当数量的线程,其造成的资源消耗仍将

是非常可观的。更重要的是,这些线程绝大多数将处于休眠状态,什么也不做,却空消

耗服务器资源。因而,需要一种更好的方式来使用线程,而不是简单的让每个线程陷入

等待。

在这种情况下,最好的解决方法就是使用线程池,暂时不需要工作的线程并不是陷入等

待,而是被回收。被回收的线程进入一个线程池中,当有新任务时,便从线程池中启用

一个线程来完成它,该线程完成任务后又将返回线程池,如此往复。同时,线程池应该

能够根据当时的负载自动增减线程数,理想情况是能够保证其缓存的线程不多不少,恰

好够用。另外一种管理线程池的方法是创建等于CPU数目的线程,目的是获得最大并发能

力,而又不浪费线程。但是,该方法在这里不适用,这里主要是希望能够同时处理所有

用户的请求(尽管会浪费一些资源),而不是最高效的利用CPU而不顾由此造成的多个用

户间的不公平性。换句话说,在多个用户同时请求服务时,尽管对每个用户的服务都将

变得缓慢,但仍然希望该实现能平均分配服务器的服务能力;而不是处理完一个再处理

一个,如果这样,某些用户将等待过长的时间,而客户端将可能超时。

按照自动调整的规则自己编写线程池是比较复杂的,而实际上Windows 2000内置了对这

种线程池的支持。这里完全可以利用Windows 2000的线程池,其好处是不言而喻的。实

际上,IIS处理ISAPI程序时也在使用线程池。

使用线程池,就意味着对同一个会话,将由不同的多个线程来处理它,因而处理会话的

程序必须是线程无关的,这还要求IIS的支持,因为IIS的相关数据也是会话处理的一部

分。 幸运的是IIS的ISAPI对此提供了很好的支持。ISAPI具有异步操作能力,初始的处

理程序通过返回PENDING,可以声明会话未结束,以使会话进入异步状态,当会话处理完

成时通过调用指定的接口来结束会话。正如前面所述,ISAPI的每个会话被一个ECB所描

述,该结构由IIS负责维护,在整个会话过程中其地址是不变的,通过一个指针,可以在

任何时候访问它,因而保证了线程无关性。

现在,对于需要接收信息的用户,如果他的推信道暂时不可用,那么必须有某种机制能

够缓存发言内容,当该用户的推信道重新变得可用时,再将缓存的发言内容发送出去。

否则该用户将丢失别人对他的发言,网络连接中断是比较常见的,如果没有这种机制,

问题将变得很严重。另外,由于IIS本身的限制,实际上ISAPI扩展程序一次只能有一个

异步操作。而新的信息可能在该异步操作的过程中到达,此时已经不可能再发起另一个

异步操作,为了使此时的发言信息不至丢失,也需要缓存机制。

现在,对于需要接收信息的用户,如果他的推信道暂时不可用,那么必须有某种机制能

够缓存发言内容,当该用户的推信道重新变得可用时,再将缓存的发言内容发送出去。

否则该用户将丢失别人对他的发言,网络连接中断是比较常见的,如果没有这种机制,

问题将变得很严重。另外,由于IIS本身的限制,实际上ISAPI扩展程序一次只能有一个

异步操作。而新的信息可能在该异步操作的过程中到达,此时已经不可能再发起另一个

异步操作,为了使此时的发言信息不至丢失,也需要缓存机制。

从表面来看,该缓存是“每用户”的,即为每个特定的用户缓存每个发言。但是,从发

言内容本身来分析,如果一个发言的多个目的用户的推信道都不可用,该发言就要在服

务器端缓存多份,这显然是不合算的。因而,要求缓存机制既要保证缓存能够区分目的

用户,又能避免重复缓存相同的内容。最终的结果是导致了一种中央缓存机制,所有发

言内容的实体都存放在中央缓存中,对每个缓存的内容,都分配一个ID,称之为消息ID。

然后,对该发言涉及的每个目的用户,都发送一个包含该ID的通知,告诉它有信息到达

,每个用户只需要一个相对小得多的本地缓冲区来缓存这些通知便可以解决推信道意外

中断的问题,从而节省了服务器资源。当推信道重新可用时,依据本地缓存中的消息ID

,便可从中央缓存取出对应的信息并发送给用户。

下面的问题是,中央缓存应当如何管理。既然只有一个中央缓存,对其的访问必然会相

当频繁。因而希望对其的访问需要尽量少的阻塞,这就要求使用尽量少的同步机制,以

及尽量使用轻量级的同步机制。实现中,将整个缓存划分为若干缓冲项,每个缓冲项缓

存一条信息,缓冲项的大小是固定的(由于中央缓存主要是缓存用户发言,因而可以通

过限制发言的最大长度来确定缓冲项的大小)。中央缓存的所有缓冲项被循环使用,即

,当用完最后一个缓冲项后,下一个即将被使用的缓冲项是第一项。在聊天室的多线程

环境中,为了使多个线程不至同时选择相同的缓冲项,可以使用互锁函数。互锁函数是

轻量级的同步机制,相对其它同步机制,互锁函数带来的性能损失是微小的。

采用这种机制,一个重要的问题就是避免消息的覆盖问题,就是指:新的消息写入到了

还在使用的消息所在的缓冲项。在实际应用中,只要合理的设置缓冲项的数目,就可以

避免或尽量减少覆盖。覆盖虽然带来风险,但是由此带来的性能提升却更可观。

同时,中央缓存还必须能够保证其分配给每个进入缓存的消息的ID是不重复的,否则如

果一个用户的推信道长时间不可用,其描述符就会长时间缓存某个消息ID,此时中央缓

存可能又将该ID分配给了新的消息,当该用户的推信道重新可用时,用户将收到错误的

信息。可见,消息ID不能简单的使用缓冲项序号。

在最终的中央缓存实现中,消息ID使用一个64位的整数表示,其中高32位为读取指针,

表示一个缓冲项索引;低32位为序列号,表示在该缓冲项上已经缓存过的消息的总数。

这样,对不同缓冲项上的消息,肯定具有不同的ID,对同一个缓冲项上不同时间进入的

消息,其ID要在该缓冲项的消息内容重复改写232次后才会重复,那时对该消息的引用应

该都已不复存在,因而在可预见的情况下,消息ID是不会重复的。缓冲项序列号也可以

使用互锁函数取得,但由于缓冲项的分配已经在一定程度上隔离了并发,在填写缓冲项

的时间不是很长的情况下,在一个线程对缓冲项操作的同时,不应该存在另一个操作同

样缓冲项的线程。因而,实际上不需要使用互锁函数,只需简单的将序列号加一即可。

当从中央缓存中提取信息时,要先检查消息ID中记录的缓冲项序号是否与实际缓冲项中

的序号一致,如果一致才开始提取信息,否则通知信息丢失。还有,缓冲项可能会在提

取信息的过程中被重新分配,此时另一个线程将同时向缓冲区中写入,必须设法检测这

种情况,否则也将会发送错误的信息。只要在存入信息之前先增量序号,在提取信息之

后再检测一次序号,就可以保证提取的信息是准确的。

中央缓存的大小是一个关键的问题,太大的缓冲区会造成资源的浪费,太小又会造成严

重的信息丢失。而且在聊天室的整个运行过程中,不同时期需要的缓存大小也是不同的

。因而需要能够动态的调整缓冲区的大小,以使其适应不同的使用强度。很不幸,这次W

indows 2000没有提供现成的功能,为此,设计了一个自动调整机制,根据每单位时间内

丢失的信息的百分率来确定缓冲区的使用强度。如果缓冲区过小,单位时间内必然有很

多覆盖出现,因而造成较多的信息丢失,如果丢失率高于某个阀值,则应该增加缓冲区

;反之,如果缓冲区过大,覆盖几乎不会出现,如果丢失率长时间低于某个阀值,则应

该减少缓冲区。可以想象,只要合理的规定两个阀值,就能使缓冲区的利用率达到最优

对于总目录、聊天室描述符和用户描述符,在同步需求方面,有一个共同的特征是,都

涉及“单个写、多个读”的同步要求。这是一种很典型的同步需求,在Jaffrey Richter

著的《Windows核心编程》中有详细的介绍,本实现使用了其中的例程。但是,在原例程

的源代码中,不支持已经获得写锁的线程再获得读锁,这样做会造成死锁。我修改了代

码,以支持这种情况,因为在实际编写代码时会用到。

使用这种锁定机制,很容易就可以实现用户描述符内的消息ID缓存。消息ID缓存需要的

是类似于中央缓存的机制——循环使用、自动覆盖、能保证信息完整性。但与中央缓存

不同的是,消息ID缓存处于用户描述符内部,其本身受到描述符同步机制的保护,完全

可以使用已有的同步机制来简化其实现。对实现的简化主要体现在保证信息的完整性上

,其主要思想就是将单写多读同步机制反过来用:获得读锁的线程实际上进行写操作,

而获得写锁的线程进行读操作。由于可以同时有多个线程获得读锁,此时使用类似中央

缓存的机制就可以保证ID缓存被循环利用,而旧的缓存项被自动覆盖。当某个线程要读

取ID时,只要获得写锁,就可以保证没有其它线程在向缓冲区中写入信息,因而也就不

需要类似中央缓存的缓冲项序号。由上面的分析可知,IIS只允许每个会话每次一个异步

I/O,而只有在会话发生I/O时才需要读取消息,因此,实际上同一时刻只有一个线程需

要从ID缓冲区中读取ID,可见使用写锁来读取数据不会降低并发性能。

接下来的问题是,为用户发送信息的I/O是异步的,那么如何来驱动它开始发送?它在发

送完成后又将做什么?当推信道刚被挂接时,就检查是否有要发送的信息,如果有,则

进行异步发送,最后返回PENDING。当异步发送完成时,回调函数被调用,它检测其刚处

理的用户描述符中是否还有要发送的消息,如果有,则立即启动下一个异步发送,如此

往复,直到没有消息可发。此时,函数当然不能进入等待,它应该立即返回,以使其所

用线程返回线程池。而当新消息到这之后到达时,异步发送已经停止,消息无法发送出

去,此时就需要写入消息ID的线程来启动异步发送。从另一方面来想,如果异步发送还

在进行,则写入ID的线程不应再启动它,否则将有两个异步操作同时发生在同一个会话

上,由于之前所说的IIS限制,这样的操作将导致错误。因而必须使写入ID的线程能够检

测异步发送是否已经挂起,可以通过用互锁函数操作一个标志变量来解决这个问题。这

时,另一个可预见的并发性问题是,如果两个线程同时向ID缓冲区中写入时,都发现异

步发送已经挂起,因而同时去启动异步发送,同样会造成两个异步操作同时存在,因而

还需要一个锁变量来使这些写入消息线程互斥,以竞选出一个线程来启动异步发送。

最后,在具体实现的过程中,一个很重要的问题就是锁定。必须为每个操作的每个步骤

安排合适的锁定强度,更要合理的安排这些步骤之间的锁定顺序。由于在同一时刻,总

会有不同种类的多个操作在进行,如果锁定顺序和强度使用不当,轻则造成性能下降,

重则造成死锁。造成死锁的原因经常就是设计时的逻辑错误,而且死锁一旦发生,其原

因将是很难被检测到的。因而,在编写代码之前,必须对其进行仔细的设计。

至此,服务器端的设计已经大体完成,至于详细到有关代码编写的问题,如前言所说,

不是本文重点。相关信息可查阅附录中的UML图。

后记在NS浏览器中,支持一种特殊类型的数据流,它的类型描述字段是:
“Content-Type: multipart/x-mixed-replace;boundary=BOUNDARY”,这一非标准数据

类型支持多个部分的数据,部分之间以boundary划分,每个部分为同一内容在不同时间

的一个快照。使用该数据类型,也许能够用纯HTML实现。但只有NS才支持它,IE却不支

持。

在我完成基于此理论的聊天室的第一版之后不久,发现首都在线(www.263.net)也使用

了类似的Push技术(到本文成文时它还在使用),只是在具体实现上有所不同。其服务

器不是NT,而是Unix系列的。它没有使用类似文中所属的嵌入Web服务的方式,似乎是写

了一个另外的服务,监听一个80以外的端口,然后让用户去连接指定的端口。该端口也

执行HTTP协议,如果整个服务都是自主开发的,则HTTP协议的服务器端实现也应该是自

己编写的,由此看来,它们的开发量应该不会小。但我不认为它的效率会比本文中的实

现高。

同样,在客户端实现上也存在差异,未完成的页面被放在了UI部分,就是说,直接接收

推信息的页面对用户是可见的,因而,它推到该页面的信息包含了许多HTML标记,实际

上,它甚至还包含了脚本。我不知道这样做有什么好处,但其能够肯定的是,其要传输

的信息量较大,而且由于包含了脚本,如果传输中出错,脚本也将执行错误,这通常比H

TML标记的错误更严重。

虽然聊天室已经实现,但是很可惜,由于很多方面的原因,到本文成文时我还不能将其

部署,但愿我能尽快的部署它。
 

  • 0
    点赞
  • 6
    收藏
    觉得还不错? 一键收藏
  • 3
    评论
评论 3
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值