《Java实用系统开发指南》 第一章

第一章:高性能聊天系统

  本章以建立一个聊天系统为例,介绍如何使用J2SE 1.4非堵塞I/O(NI/O)为核心开发一个高性能的实时互动服务器系统,这个高性能服务器系统可以拓展为更广阔的应用,如游戏系统、社区系统或者数据实时采集系统。

1.1.        系统需求

  聊天交流是目前互联网提供的主要内容。聊天系统有多种实现方式,类似ICQ属于一种点对点的聊天系统,还有一种是基于Socket的集中式聊天系统,这种聊天系统需要登录统一的聊天服务器,每个人的聊天信息其他人都可以看到,类似一种会议室,当然,两个人之间也可以进行保密的私语。

  在基于Socket的聊天系统中,主要有两种角色:服务器和客户端,不同的客户端登录集中式的服务器,通过服务器将一个客户端发出的信息推送到其他所有客户端。

  基于Socket的聊天系统最早实现是使用网页刷新方式,通过客户端不断地自动刷新,将服务器端整个页面内容下载到客户端显示,这种方式的聊天速度慢,而且有刷屏现象,很快被更新的聊天技术所替代。

  聊天系统在客户端和服务器之间主要传送的是文字信息,服务器端只需要把最新的文字信息推送到客户端,这样减少了网络传输内容,节省了网络传输的时间,无疑提高了聊天速度。这种“推”技术是目前基于Socket聊天系统的主要实现技术。

  一个基于Socket的聊天系统有下列具体功能要求:

  (1)客户端和服务器必须保持随时随地的连接。这有别于普通Web浏览的连接方式。在使用浏览器访问服务器时,先由客户端发出HTTP协议,然后服务器响应处理这个客户端的响应,再返回处理结果;请求(Request)和响应(Response)是一种一对一的前后因果关系。

  而在基于Socket的聊天系统中,客户端发出聊天信息的同时,客户端也在接受服务器发送过来的其他人的聊天信息,因此,请求和响应不存在那种前后对应关系,是两种分别独立进行的进程。

  因为服务器任何时候都可能发送信息到客户端,因此,客户端和服务器一旦建立连接,必须能让服务器在以后发送中寻找定位到这个连接。

  (2)在速度性能方面,聊天系统提出了更高的要求。在网络连接的薄弱环节I/O通信方面,要求能够实现无堵塞地、流畅地数据读写。在面对几百个甚至更多的客户端同时发出连接信息的情况下,服务器要求能够保持高性能的并发处理机制,迅速地完成这几百个并发请求的处理和发送任务。

    (3)在扩展性和伸缩性方面,聊天系统也提出了一定的要求。当一台服务器不能满足要求时,必须在客户端不知晓的情况下,通过不断增加服务器就能方便地拓展聊天系统的整体处理能力。对于客户端用户来说,这些服务器群都象征一个统一的服务器,不需要他们在进入聊天室之前先选择具体的服务器;也没有单个聊天室最大人数的限制,如果可以,服务器群可以支撑一个巨大容量的聊天室。


1.2        架构设计

  本系统的设计核心是Socket底层通信,基于快速稳定的Socket底层通信架构,不但可以实现聊天系统,还可以实现其他如游戏、数据采取等实时性要求较高的系统,甚至可以建立一个快速的平台服务器系统。相比J2EE服务器系统,该平台系统的最大优势就是精简的代码带来的高性能。

当然,如果单纯追求高性能和速度,也许直接使用汇编就可以。使用Java设计这样的实时系统,实际还有一种很重要的目的,即追求高度的可扩展性和伸缩性。

因此,本系统设计必须将高性能和高伸缩性两个方面和谐地统一起来,不能盲目追求性能而破坏面向对象的编程风格和模式;也不能因为追求更大的重用性,建立太多复杂的中间层,其实这方面J2EE已经做得很好,有关J2EE的应用将在以后章节重点讨论。

当然,高性能应该是本系统的主要特色,为了实现本系统高效率的并发处理性能,设计上将采取Reactor模式实现自我快速触发;网络通信上,使用非堵塞I/O进行流畅地数据读写,在应用逻辑上,通过非堵塞的多线程技术实现并发功能处理。特别是J2SE 1.4以后版本推出的非堵塞I/O,将使得Java表现出和C语言同样的优越性能。


1.2.1          JAVA事件模型

  事件只有在被触发时才会发生,它的发生是程序系统无法预料和计划的。例如,火警探测器只有在发生火警时才会触发,但是火警的发生是无法事先预料的,它处于时刻可能发生当中。为了能响应这些无法预料发生的事件,必须建立一套事件处理机制,以预备在发生事件时实现相应的处理


1.2.2          架构设计图

  考虑到系统的可重用性和伸缩性,需要将本系统的网络通信底层和应用系统分离开。这样,基于可重用的网络通信层,可以实现其他各种实时性较高的应用系统,同时,系统还需要提供一些基本功能支持,如网络连接状态管理以及用户状态相关管理,前者为实现一个动态的实时在线系统提供基本连接的管理,后者类似J2EE中Servlet部分的Session管理。


1.2.3          协议设计

  TCP是一种面向连接的协议,传输数据比较可靠。TCP协议中包含了专门的传递保证机制:当接收方收到发送方传来的信息时,会自动向发送方发出确认消息;发送方只有在接收到该确认消息之后才正式传送数据信息,否则将一直等待直到收到确认信息为止。

  UDP是面向非连接的协议,传送数据之前不需要建立专门的连接,直接发送就可以,因此速度要比TCP快。由于UDP协议并不提供数据传送的保证机制,因此可能发生丢包的情况。UDP适合一次只传送少量数据、对可靠性要求不高的应用环境。


1.2.4          多线程

  在 Java 程序中使用多线程要比在 C 或 C++ 中容易得多,因为 Java 编程语言提供了语言级的支持,但是这并非意味着在使用时可以避开线程的一些基本问题。在以后章节中介绍的JSP/Servlet容器,实际是一个线程池容器,JSP在运行时将编译成Servlet,而Servlet是一种线程类,J2EE通过Servlet概念的提出,确保开发者不用担心线程以及同步等问题,可以像往常一样编程。


1.2.5          线程池

  1.2.4节主要介绍了多线程使用时对资源争夺情况的处理,只要掌握几个基本原则,在一般情况下使用多线程实现应用处理就没有太大问题。


1.2.6          非堵塞I/O NoneBlock I/O

   在前面讨论中,非堵塞I/O 作为Reactor模式的实现,实际上提供了一种事件发生、自我激活、主动通知的机制。因此使用非堵塞I/O将大大提高系统的I/O处理性能。 非堵塞I/O中有3个重要的类:Selector、SelectionKey 和Channel。


1.3           Socket核心设计和实现

1.3.1          TCPReactor模式
1.3.2          UDP实现
1.3.3          客户端实现

1.4           Socket接口设计和实现

1.4.1   队列和对象类型
1.4.2   访问者模式定义

   Visitor模式表示作用于某对象集合中各元素的操作。这种操作不是一种原生的,而是使用了访问者模式后才有的方法,即这些元素原本没有做好接受这种操作的准备。因此,没有提供一种专门方法用于外界统一访问,如果强行需要对这些元素进行访问,那么就需要访问者模式为这些元素再设计一个统一接口。这样,通过这个统一接口提供的统一方法,外界可以对这些元素实现统一访问了。


1.4.3   访问者模式实现
1.4.4   
协议封装
1.4.5   
重整Refactoring

   在这种情况下,就开始要考虑重整(Refactoring),消灭这个隐患。决定重整之前,如果还有其他因素促进重整就更好了,Socket I/O读写部分功能有两个,整个Socket底层有4处代码:UDPClient、服务器端的UDPHandler、TCPCLient和服务器端的TCPHandler,每个类中有两次Socket读写操作,Socket读写操作代码发生重复两次以上。

当一种现象重复3次以上时,就需要使用重整了。


1.5   
应用接口设计和实现
1.5.1   Connection API
 
1.5.2   ConnectionFactory API
1.5.3   TcpConnection