2005年11月18日

      这是一篇写给初学者看的文章。在前一段时间的招聘软件设计师的过程中,我对每一个看似初学者的人都会问这个问题,“您觉得平台相关性和平台无关性哪个更好一些”,呵呵(偷笑),其实这是唬人的,多数回答者都会顺着出题者假装的思路回答“我个人认为平台无关性比较好”,可是只要有点软件设计经验或是对这个问题有所思考的人都知道其实这个问题不只两个标准答案。

      关于平台无关性,我不想说什么,说什么也没用。大量软件设计或软件架构以此来标榜自己的优秀和出众,其实这没什么,因为大部分平台无关性的工作不是由你来做的。如果你正在写一个Java程序,并依照Sun的100% Pure Java的要求来做,那么应该就是平台无关的,如果你正在写一个Eclipse应用或直接用SWT/JFace组合来写应用程序,那么也是平台无关的,如果你喜欢C/C++,并在用wxWidget写应用程序,那么也是平台无关的,如果你实在是很牛,在依照OSGi的规范写代码,那么ok,你的程序已经可以从微设备到大型机统统可以用。

      平台无关也是一个相对的概念,在多个操作系统上运行可以称为平台无关的,以往在多个不同品种的CPU上运行可以称为平台无关性,还有一件搞笑的事情,某个公司出了一套系统,可以在Java和.NET两个平台上运行,但却只能依赖于Windows系统(因而只能依赖于x86芯片),居然还可以称平台无关性,可见这个概念有多么混乱。我个人评价是不是平台无关的仅有一个标准,那就是——你有没有为平台无关做出贡献!如果你为了能在多个平台上跑出你的代码而做了很多工作,那么你就可以称自己为平台无关的,而如果你仅仅依赖SWT工作,那就不能称自己为平台无关的。不过话又说回来,如果你把SWT包含在自己的软件中,并为此出了该软件的多个版本(每个SWT的版本是一个发行包),那么你也可以称此为平台无关的,虽然这个贡献并非出自你手。

      平台无关也不见得总是个优点,很多系统为了坚持平台无关而牺牲了很多特性,或不必要的提高了成本。比如前几年很多系统原意搞WEB界面,导致了很多易用性方面的问题,Cooper说Web使人机交互技术倒退了10年,的确如此。我还读过一段源码,大概是一个单机版个人软件的源码,令人惊奇的是,该软件很小,却把很多笔墨花在了业务对象和JDBC访问层之间的一个“抽象数据存储层”,理由是便于将来移至非JDBC平台,天哪!会有多少用户有机会使用不支持 JDBC的数据库??!!这种设计和下面一种设计是一样的效用:“为了让这段代码支持非OO语境,我决定整个软件只用一个类!”。这种追求可以用四个字来表述就是:“过渡设计”或者“吃饱撑着”!

      相反,这个世界上有80%的软件是平台相关的,这没什么不能理解的。就像“民主和专制的TCO哪个高”这个问题的答案一样,如果我现在要招聘的是部门经理或副总裁,我很可能会问这个问题。事实上是,在整个人类的发展历程中,总成本最低(即总效率最高)的几个“社会时期”,几乎全是专制,但如果你不假思索,你的答案一定是民主!当然,平台相关也是相对的概念。

      说到这里,有兴趣的读者可能会说结合二者是最好的选择,我不喜欢这种说法,因为太辨证了,我喜欢的是首先考虑依赖于哪个框架,再找寻该框架的平台无关性,如果没有必要,尽量不要为平台无关(实际上是一种优先级非常低的非功能性需求)做任何事情,但如果有必要且成本允许,再做少许考虑,最好还是能够重用开源世界的产品。

      仍以OSGi为例,这个例子很好,它对Java语言本身(还不是面向对象的公共语义)非常依赖,直接依赖至VM的spec,当然也写了些代码以避开 ClassLoader的个性,即使如此,OSGi事实上实现了从微设备到大型机全套支持,借助Java的平台无关性,既没有易用性、性能和成本方面的丝毫损失,也为上层平台提供了平台无关的环境。同样,为Mac OSX设计的很多非常优秀的软件都没有考虑平台无关的问题,而是用在PC上再做一套的方式来解决,这些都是值得思考和借鉴的解决方案。

转自Brian Sun @ 爬树的泡泡[http://www.briansun.com]

2005年11月15日

Java 语言的Calendar(日历),Date(日期), 和DateFormat(日期格式)组成了Java标准的一个基本但是非常重要的部分. 日期是商业逻辑计算一个关键的部分. 所有的开发者都应该能够计算未来的日期, 定制日期的显示格式, 并将文本数据解析成日期对象. 我们写了两篇文章,这是第一篇, 我们将大概的学习日期, 日期格式, 日期的解析和日期的计算.   

我们将讨论下面的类: 

1、具体类(和抽象类相对)java.util.Date 
2、抽象类java.text.DateFormat 和它的一个具体子类,java.text.SimpleDateFormat 
3、抽象类java.util.Calendar 和它的一个具体子类,java.util.GregorianCalendar 

具体类可以被实例化, 但是抽象类却不能. 你首先必须实现抽象类的一个具体子类. 

Date 类从Java 开发包(JDK) 1.0 就开始进化, 当时它只包含了几个取得或者设置一个日期数据的各个部分的方法, 比如说月, 日, 和年. 这些方法现在遭到了批评并且已经被转移到了Calendar类里去了, 我们将在本文中进一步讨论它. 这种改进旨在更好的处理日期数据的国际化格式. 就象在JDK 1.1中一样, Date 类实际上只是一个包裹类, 它包含的是一个长整型数据, 表示的是从GMT(格林尼治标准时间)1970年, 1 月 1日00:00:00这一刻之前或者是之后经历的毫秒数. 


一、创建一个日期对象 

让我们看一个使用系统的当前日期和时间创建一个日期对象并返回一个长整数的简单例子. 这个时间通常被称为Java 虚拟机(JVM)主机环境的系统时间. 
[code:1:ad22b58018]import java.util.Date; 

public class DateExample1 { 
public static void main(String[] args) { 
// Get the system date/time 
Date date = new Date(); 

System.out.println(date.getTime()); 


[/code:1:ad22b58018]

在星期六, 2001年9月29日, 下午大约是6:50的样子, 上面的例子在系统输出设备上显示的结果是 1001803809710. 在这个例子中,值得注意的是我们使用了Date 构造函数创建一个日期对象, 这个构造函数没有接受任何参数. 而这个构造函数在内部使用了System.currentTimeMillis() 方法来从系统获取日期. 

那么, 现在我们已经知道了如何获取从1970年1月1日开始经历的毫秒数了. 我们如何才能以一种用户明白的格式来显示这个日期呢? 在这里类java.text.SimpleDateFormat 和它的抽象基类 java.text.DateFormat 就派得上用场了. 


二、日期数据的定制格式 

假如我们希望定制日期数据的格式, 比方星期六-9月-29日-2001年. 下面的例子展示了如何完成这个工作:
[code:1:ad22b58018]import java.text.SimpleDateFormat; 
import java.util.Date; 

public class DateExample2 { 

public static void main(String[] args) { 

SimpleDateFormat bartDateFormat = 
new SimpleDateFormat("EEEE-MMMM-dd-yyyy"); 

Date date = new Date(); 

System.out.println(bartDateFormat.format(date)); 


[/code:1:ad22b58018]
只要通过向SimpleDateFormat 的构造函数传递格式字符串"EEE-MMMM-dd-yyyy", 我们就能够指明自己想要的格式. 你应该可以看见, 格式字符串中的ASCII 字符告诉格式化函数下面显示日期数据的哪一个部分. EEEE是星期, MMMM是月, dd是日, yyyy是年. 字符的个数决定了日期是如何格式化的.传递"EE-MM-dd-yy"会显示 Sat-09-29-01. 请察看Sun 公司的Web 站点获取日期格式化选项的完整的指示.


三、将文本数据解析成日期对象 

假设我们有一个文本字符串包含了一个格式化了的日期对象, 而我们希望解析这个字符串并从文本日期数据创建一个日期对象. 我们将再次以格式化字符串"MM-dd-yyyy" 调用SimpleDateFormat类, 但是这一次, 我们使用格式化解析而不是生成一个文本日期数据. 我们的例子, 显示在下面, 将解析文本字符串"9-29-2001"并创建一个值为001736000000 的日期对象. 

例子程序: 
[code:1:10d3268d34]import java.text.SimpleDateFormat; 
import java.util.Date; 

public class DateExample3 { 

public static void main(String[] args) { 
// Create a date formatter that can parse dates of 
// the form MM-dd-yyyy. 
SimpleDateFormat bartDateFormat = 
new SimpleDateFormat("MM-dd-yyyy"); 

// Create a string containing a text date to be parsed. 
String dateStringToParse = "9-29-2001"; 

try { 
// Parse the text version of the date. 
// We have to perform the parse method in a 
// try-catch construct in case dateStringToParse 
// does not contain a date in the format we are expecting. 
Date date = bartDateFormat.parse(dateStringToParse); 

// Now send the parsed date as a long value 
// to the system output. 
System.out.println(date.getTime()); 

catch (Exception ex) { 
System.out.println(ex.getMessage()); 



[/code:1:10d3268d34]


四、使用标准的日期格式化过程 

既然我们已经可以生成和解析定制的日期格式了, 让我们来看一看如何使用内建的格式化过程. 方法 DateFormat.getDateTimeInstance() 让我们得以用几种不同的方法获得标准的日期格式化过程. 在下面的例子中, 我们获取了四个内建的日期格式化过程. 它们包括一个短的, 中等的, 长的, 和完整的日期格式. 
[code:1:10d3268d34]import java.text.DateFormat; 
import java.util.Date; 

public class DateExample4 { 

public static void main(String[] args) { 
Date date = new Date(); 

DateFormat shortDateFormat = 
DateFormat.getDateTimeInstance( 
DateFormat.SHORT, 
DateFormat.SHORT); 

DateFormat mediumDateFormat = 
DateFormat.getDateTimeInstance( 
DateFormat.MEDIUM, 
DateFormat.MEDIUM); 

DateFormat longDateFormat = 
DateFormat.getDateTimeInstance( 
DateFormat.LONG, 
DateFormat.LONG); 

DateFormat fullDateFormat = 
DateFormat.getDateTimeInstance( 
DateFormat.FULL, 
DateFormat.FULL); 

System.out.println(shortDateFormat.format(date)); 
System.out.println(mediumDateFormat.format(date)); 
System.out.println(longDateFormat.format(date)); 
System.out.println(fullDateFormat.format(date)); 


[/code:1:10d3268d34]

注意我们在对 getDateTimeInstance的每次调用中都传递了两个值. 第一个参数是日期风格, 而第二个参数是时间风格. 它们都是基本数据类型int(整型). 考虑到可读性, 我们使用了DateFormat 类提供的常量: SHORT, MEDIUM, LONG, 和 FULL. 要知道获取时间和日期格式化过程的更多的方法和选项, 请看Sun 公司Web 站点上的解释. 

运行我们的例子程序的时候, 它将向标准输出设备输出下面的内容: 
9/29/01 8:44 PM 
Sep 29, 2001 8:44:45 PM 
September 29, 2001 8:44:45 PM EDT 
Saturday, September 29, 2001 8:44:45 PM EDT


五、Calendar 类 

我们现在已经能够格式化并创建一个日期对象了, 但是我们如何才能设置和获取日期数据的特定部分呢, 比如说小时, 日, 或者分钟? 我们又如何在日期的这些部分加上或者减去值呢? 答案是使用Calendar 类. 就如我们前面提到的那样, Calendar 类中的方法替代了Date 类中被人唾骂的方法. 

假设你想要设置, 获取, 和操纵一个日期对象的各个部分, 比方一个月的一天或者是一个星期的一天. 为了演示这个过程, 我们将使用具体的子类 java.util.GregorianCalendar. 考虑下面的例子, 它计算得到下面的第十个星期五是13号. 
[code:1:041aeb23d1]import java.util.GregorianCalendar; 
import java.util.Date; 
import java.text.DateFormat; 

public class DateExample5 { 

public static void main(String[] args) { 
DateFormat dateFormat = 
DateFormat.getDateInstance(DateFormat.FULL); 

// Create our Gregorian Calendar. 
GregorianCalendar cal = new GregorianCalendar(); 

// Set the date and time of our calendar 
// to the system&s date and time 
cal.setTime(new Date()); 

System.out.println("System Date: " + 
dateFormat.format(cal.getTime())); 

// Set the day of week to FRIDAY 
cal.set(GregorianCalendar.DAY_OF_WEEK, 
GregorianCalendar.FRIDAY); 
System.out.println("After Setting Day of Week to Friday: " + 
dateFormat.format(cal.getTime())); 

int friday13Counter = 0; 
while (friday13Counter <= 10) { 

// Go to the next Friday by adding 7 days. 
cal.add(GregorianCalendar.DAY_OF_MONTH, 7); 

// If the day of month is 13 we have 
// another Friday the 13th. 
if (cal.get(GregorianCalendar.DAY_OF_MONTH) == 13) { 
friday13Counter++; 
System.out.println(dateFormat.format(cal.getTime())); 
} } } } 

在这个例子中我们作了有趣的函数调用: 
cal.set(GregorianCalendar.DAY_OF_WEEK, 
GregorianCalendar.FRIDAY); 

和: 
cal.add(GregorianCalendar.DAY_OF_MONTH, 7); 

set 方法能够让我们通过简单的设置星期中的哪一天这个域来将我们的时间调整为星期五. 注意到这里我们使用了常量 DAY_OF_WEEK 和 FRIDAY来增强代码的可读性. add 方法让我们能够在日期上加上数值. 润年的所有复杂的计算都由这个方法自动处理. 

我们这个例子的输出结果是: 
System Date: Saturday, September 29, 2001 
当我们将它设置成星期五以后就成了: Friday, September 28, 2001 
Friday, September 13, 2002 
Friday, December 13, 2002 
Friday, June 13, 2003 
Friday, February 13, 2004 
Friday, August 13, 2004 
Friday, May 13, 2005 
Friday, January 13, 2006 
Friday, October 13, 2006 
Friday, April 13, 2007 
Friday, July 13, 2007 
Friday, June 13, 2008 


六、时间掌握在你的手里 

有了这些Date 和Calendar 类的例子, 你应该能够使用 java.util.Date, java.text.SimpleDateFormat, 和 java.util.GregorianCalendar 创建许多方法了. 

在下面的文章中, 我们将讨论更高级的Date 和 Calendar 类的使用技巧, 包括时区和国际化的格式. 我们还会考察两个日期类 java.sql.Date 和 java.util.Date 之间的区别.

2005年11月10日

2001 年 10 月 16 日

网络通信是当今信息社会网络化一个必不可少的环节。各种为Internet量身定做的网络通信信息技术层出不穷。如早期的CGI,ISAPI等,现在非常流行的JSP,Servlet,ASP等,特别是Sun MicroSystem公司的J2EE和Microsoft的。NET方案,为企业快速高效的实现Internet应用提供了强大支持。而对于一些基于Internet的即时通信,一般是采用C/S模式。即客户端安装并执行客户程序,通过某种通信协议与服务器端的服务器程序或者是直接与另外的客户程序进行通信。本文介绍的是怎样采用Java技术,用B/S模式来实现基于Internet的即时通信,对学习Java Socket编程很有帮助。

为什么选择Java 和 B/S
Sun MicroSystem 的Java技术以其明显的优势得到了广泛应用。如美国华尔街的高盛,美国Amazon.com以及美国通用等的电子商务网站都是采用Java技术。Java语言具有面向对象,面向网络,可移植,与平台无关,多线程,安全等特点。基于网络带宽限制和网络安全等原因,本即时通信系统的客户端用Java小程序(applet)来实现。即通过支持Java的浏览器(如Microsoft Internet Explorer 和 Netscape Navigator等)下载并执行Java Applet 来完成客户端操作。Java Applet 具有体积小,安全等特点。通常,基于C/S模式的通信程序都要求用户先下载一个客户端程序安装在本地机上,而且这个客户程序相对比较大(所谓“胖客户”)。而且,对于一些不可信站点的程序还要考虑到安全因素,因为大多数后门工具就是利用这个缺陷侵入用户计算机的。而使用Java Applet,用户就不必为这些而烦恼。首先,Applet通常很小,用户不必先安装就可立即执行。其次,由于Applet的安全性,用户可以放心使用来自Internet的哪怕是不可信站点的Applet程序。这样,客户端只要拥有支持Java的浏览器就可实现。根据现在的情况来看是不难办到的。而在服务器端我们采用Java Application。这样可以充分发挥Java技术的多线程及平台无关等优点,并且在后方可以借助JDBC与DBMS进行通信,存储和更新数据,以及采用J2EE等技术进行扩展。本文重点放在即时通信,因此服务器端与DBMS的连接技术将不作介绍。

系统设计与实现
开发平台及工具:Windows2000,Jbuilder4(J2SDK1.3),Together4.2。
客户端程序运行环境:拥有支持Java的浏览器的任何平台。
服务器程序运行环境:拥有JVM的任何平台。

1. 需求分析
图2。1。1为系统的Use Case图。

图2。1。1
图2。1。1

用例(Use Case): 远程会话

系统范围(Scope):服务器端系统

级别(Level):顶层(Summary)

外界系统描述(Context of Use):为了与该系统交互,外界系统为远程客户,而每一客户对应于一个客户端程序,客户通过客户端程序与系统交互

主要执行者(Primary Actor):客户

典型成功场景(Main Success Scenario):

  1. 客户通过客户端程序发出"申请新账号"的请求,并提供登录账号和登录密码等申请信息;
  2. 服务器端程序对该客户提交的信息进行验证,并发送"账号申请成功"信息;
  3. 客户接收"申请账号成功"信息后,继续发送请求;
  4. 服务器端程序接收该请求并进行相应处理,然后将执行结果返回给客户;
  5. 重复执行步骤3和步骤4,直到客户发送"会话结束"信息。这时服务器程序完成结束前的处理工作后,断开与客户的连接;

扩展(Extensions):

  • 1a.:系统发现该账号已经存在

    1a.1.:系统返回"该账号已存在"信息,客户可以选择另选账号或者退出

  • 1b:客户提交"登录"信息:
    • 1b.1.:系统对客户身份进行验证:
      • 1b.1a.:验证通过,返回"登录成功"信息
      • 1b.1b:验证不能通过,返回"登录失败"信息,客户可以再尝试登录或者退出
  • 说明:典型成功场景的第1步可以用1a代替,接下来是1a.1;或者用1b代替,后接1b.1,再接1b.1a或者1b.1b。

2. 概要设计

(图2。2。1)
2. 概要设计(图2。2。1)

该系统分为两大部份:客户端程序和服务器端程序。客户端程序采用Java 小程序,通过socket与服务器端程序通信;服务器端程序采用Java Application,同样采用socket与客户端程序进行交互。考虑到即时通信的准确性要求,通信协议采用TCP。

3. 详细设计

  1. 服务器端程序设计:

    服务器端完成的功能是:对服务器的某一可用端口进行监听,以获得客户端请求,从而对客户端请求进行处理。因为是多客户同时请求,所以要采用多线程,为每一个在线用户分配一个线程,实时处理每个客户端的请求。因此,

    对服务器端程序抽象如下:(图2。3。1)

    • a.公共数据处理(Common Data Processing)

      处理公共数据。如在线人数统计,客户的公共数据(如通知等),客户数据资料的存储与读取等(与数据库交互);

    • b. 端口监听器(Port Listener)

      监听服务器某一端口,为每一在线客户建立一个会话线程;

    • 客户请求处理(Client Request Processing)

      处理客户的请求。根据客户的请求执行相应的操作。

    • 服务器管理器


    服务器端的管理工具,如对数据进行统计,紧急情况的处理等。

    服务器端类的设计(图2。3。2和图2。3。3):

    公共数据处理类CmDataProcessor(Common Data Processor):该类包含客户所共有的数据,以及如何对这些数据进行处理。

    端口监听类PortListener(Port Listener):该类实现了java. lang. Runnable接口,从服务器程序初始化完成后一直运行。由于目前JDK只支持同步通信,在没有客户请求时,该线程处于等待状态;一旦有客户请求到来,便继续执行。这时服务器程序可以通过java. net. ServerSocket. accept()方法获得客户端请求的java. net. Socket对象。然后用这个Socket对象为参数构造一个新的线程:ClientSession的实例(类ClientSession以下将作介绍)。然后在ClientSession实例中用该Socket对象构造一个输出流java. io. PrintStream和一个输入流java. io. BufferedReader,以后,每个客户就可以通过这一对输入输出流与服务器交互了。应该注意的是,ServerSocket 对象并不是在该对象内创建的,而是在服务器程序初始化时创建的。因为socket是进程间的通信,在线程中创建将会失败。客户端程序也是如此。

    客户会话类ClientSession(Client Session):该类继承自java. lang. Thread类,由PortListener创建。一般的,每一个在线客户都对应一个ClientSession的实例。该类用parseRequest()方法解析客户发来的请求,进行相应处理。该线程在客户会话期间一直运行,通过I/O流读取和发送数据(I/O流即从PortListener监听线程获得的java. net. Socket对象而创建的java. io. PrintStream和java. io. BufferedReader实例),直到客户退出才撤销。该类和类ClientSession一样都实现了java. lang. Runnable接口,故都有一个run()方法。该方法的结束标志着该线程将结束。

    服务器管理类 ServerManager(Server Manager):管理服务器。拥有管理权限的客户(管理员)可以远程操作服务器程序,包括运行、停止服务器,广播通知,给指定客户发送消息等特权操作。

    图2。3。2
    图2。3。2

    图2。3。3

  2. 客户端程序设计(图2。3。4)

    客户端完成的功能是:建立与服务器的连接;向服务器发送功能请求,接收来自服务器的信息,完成与主机或其他客户交互;断开与服务器的连接。客户端程序相对服务器端程序来说属于LightWeight(轻量级)。这是由本系统的自身特点决定的。所以,对客户端程序抽象如下:

    1. 客户请求发送器:负责功能请求的发送。如登录请求等。
    2. 服务器信息接收器:负责接收来自服务器端的信息。如请求处理结果等。


    客户端类的设计:
    请求发送器(RequestSender):该类发送客户端的功能请求。客户通过客户端用户界面提交要执行的操作,然后由该类将客户提交的信息封装成服务器端程序可以理解的功能请求发送出去。

    信息接收器(Receiver):该类接收来服务器端的信息。这些信息可以是客户请求的处理结果,也可以是服务器端的广播通知。为保证实时性,该类实现了java. lang. Runnable接口。在客户会话期间,该类将一直运行,实时的将来自服务器端的信息反馈给客户。该类接收信息后,应该对该信息做相应处理。如通知客户已登录成功等。这些操作都将在run()方法中实现。

    图2。3。4
    图2。3。4

    4. 实现
    以上的系统设计是一个即时通信系统的总体框架,根据实际情况,可以添加或者修改。下文就以“远程会议系统“为例来实例化这样一个通信系统。

    我们知道,远程会议系统有几个方面的特点:实时交互;准确传输信息;多客户等。所以,完全可以用该系统框架来实现(这里只给出核心代码)。

    首先我们来实现服务器端程序。为了便于对服务器程序的管理,服务器端程序采用了GUI界面。在该程序初始化时应该实现对可用端口的监听(程序清单1。1)。

    程序清单1。1。1

        try {
              socket = new ServerSocket(NetUtil.SLISTENPORT);
            }
    catch (Exception se)
        {
                statusBar.setText(se.getMessage());
            }

    其中,NetUtil.SLISTENPORT是服务器的一个可用端口,可以根据实际情况确定。NetUtil是一个接口,其中包含了该系统用到的各类常数。NetUtil.SLISTENPORT就是NetUtil中的一个整型常数。如果端口监听抛出异常,GUI中的statusBar将给出提示。监听成功后可以启动监听线程了(程序清单1。1。2)。

    程序清单1。1。2

    listenThread=new Thread (new ListenThread ());
    listenThread.start();
    

    以上程序中listenThread是一个Thread实例,用来操作监听线程。监听线程实现如下(程序清单1。2。1):

    程序清单1。2。1

       public class PortListener implements Runnable{
        ServerSocket socket; //服务器监听端口
        FrmServer frm; //FrmServer为GUI窗口类
        ClientSession cs; //客户端会话线程
        PortListener (FrmServer frm,ServerSocket socket) {
          this.frm=frm;
          this.socket=socket;
        }
          public void run () {
            int count = 0;
            if (socket == null) {
                frm.statusBar.setText("socket failed!");
                return;
            }
            while (true) {
                try {
                    Socket clientSocket;
                    clientSocket = socket.accept(); //取得客户请求Socket
                    //用取得的Socket构造输入输出流
                    PrintStream os = new PrintStream(new
                    BufferedOutputStream(clientSocket.getOutputStream(),
                    1024), false);
                    BufferedReader is = new BufferedReader(new
                    InputStreamReader(clientSocket.getInputStream()));
                    //创建客户会话线程
                    cs = new ClientSession(this.frm);
                    cs.is = is;
                    cs.os = os;
                    cs.clientSock = clientSocket;
                    cs.start();
                    } catch (Exception e_socket) {
                    frm.statusBar.setText(e_socket.getMessage());
                }
            }
        }
    }

    监听线程一直在后台运行。当有客户请求到来时,监听线程创建与该客户进行会话的ClientSession实例。这时,监听线程会等待另外客户请求的到来,然后又创建会话线程,如此循环下去。客户会话则通过会话线程进行。客户会话线程主要的工作就是怎样处理客户请求。ClientSession. parseRequest ()就是处理客户请求的。这个方法的内容应该根据实际应用的需要来确定。这里只实现了一些很简单的功能。如会议大厅发言,私下交谈等。ClientSession的实现代码见程序清单1。3。1。

    程序清单1。3。1

    public class ClientSession extends Thread {
        FrmServer frm; //GUI窗口类
        BufferedReader is = null; //输入流
        PrintStream os = null; //输出流
        Socket clientSock = null; //客户请求Socket
        int curContent = 0;
        public ClientSession (FrmServer frm) {
          this.frm=frm;
        }
        public void run () {
            parseRequest(); //处理客户请求
        }
        public void destroy () {
            try {
                clientSock.close();
                is.close();
                os.close();
            } catch (Exception cs_e) {}
        }
        //客户请求处理方法,只实现了大厅发言及私下交谈
        private void parseRequest () {
            String strTalking;
            String strUserID;
            String strMain = "";
            String strPerson = NetUtil.PERSONINFO + "\n";
            boolean flagEndProc = false;
            while (true) {
                try {
                    while ((strTalking = new String(is.readLine())) != null) {
                        if(strTalking.equals(NetUtil.CHATSTART)){ //客户会话开始
                            strUserID = new String(is.readLine());
                            // 将所有谈话内容发送给刚登录的客户
                            for (int i = 0; i < frm.dataProcessor.vecMainTalk.size(); i++) {
                                strMain += (String)frm.dataProcessor.vecMainTalk.get(i);
                                strMain += "\n";
                            }
                            curContent = frm.dataProcessor.vecMainTalk.size();
                            os.println(strMain);
                            os.flush();
                            for (int j = 0; j < frm.dataProcessor.vecP2PInfo.size(); j++) {
                                strPerson += (String)frm.dataProcessor.vecP2PInfo.get(j);
                                strPerson += "\n";
                            }
                            os.println(strPerson);
    os.flush(); //将所有在线用户名单发给新加入的客户
    os.println(NetUtil.DIVIDEDLINE);
    os.flush();
    while (true) {
    this.sleep(1000);
    String strContent = "";
    //如果有人发言,则把发言发给在线客户
    if (frm.dataProcessor.vecMainTalk.size() > curContent) {
    for (int ci = curContent; ci <frm.dataProcessor.vecMainTalk.size(); ci++) {
    strContent +=(String)frm.dataProcessor.vecMainTalk.get(ci);
    strContent += "\n";
    }
    curContent = frm.dataProcessor.vecMainTalk.size();
    os.println(strContent);
    os.flush();
    }
    //如果有人私下交谈,则把交谈的内容发给交谈的另一方
    if (strUserID != null) {
    int nvi = 0;
    for (nvi = 0; nvi <
    frm.dataProcessor.vecSelfInfo.size()&& !((String)((Vector)frm.dataProcessor.vecSelfInfo.get
    (nvi)).get(0)).equals(strUserID); nvi++);
    if (nvi < frm.dataProcessor.vecSelfInfo.size()) {
    Vector vecTalk =(Vector)frm.dataProcessor.vecSelfInfo.get(nvi);
    if ((String)vecTalk.get(1)).equals(NetUtil.CALLED)){
    String strCallRes = NetUtil.ISCALLED+ "\n";
    String strCallTemp = (String)vecTalk.get(2);
    strCallRes += strCallTemp;
    os.println(strCallRes);
    os.flush();
    }
    else if(((String)vecTalk.get(1)).equals(NetUtil.CALLING)){
    if(((String)vecTalk.get(3)).equals(NetUtil.RESPONSING)){ //一客户呼叫另一客户并有了回应
    String strResponsing =NetUtil.RESPONSE + "\n";
    String strResponsingT =(String)vecTalk.get(2);
    strResponsing += strResponsingT;
    os.println(strResponsing);
    os.flush();
    //设置客户“正在私下交谈”状态
    vecTalk.setElementAt(NetUtil.CHATTING, 1);
    String strOther = (String)vecTalk.get(2);
    int setvi = 0;
    for (setvi = 0; setvi <frm.dataProcessor.vecSelfInfo.size()
    && !((String)((Vector)frm.dataProcessor.vecSelfInfo.get(setvi)).get(0)).equals(strOther); setvi++);
    Vector vecOther = (Vector)frm.dataProcessor.vecSelfInfo.get(setvi);
    vecOther.setElementAt(NetUtil.CHATTING,1);
    }}
    else if (((String)vecTalk.get(1)).equals(NetUtil.CHATTING)) {
    String strCurContent = (String)vecTalk.get(4);
    if (!strCurContent.equals(NetUtil.NONCONTENT)) {
    String strToWho = vecTalk.get(2)+ "\n";
    String strPerRes = NetUtil.PERSONALRECEIVE+ "\n";
    strPerRes += strToWho;
    strPerRes += strCurContent;
    os.println(strPerRes);
    os.flush();
    vecTalk.setElementAt(NetUtil.NONCONTENT,4);
    }}}}}}
    // 处理客户发来与另一客户私下交谈的请求
    else if (strTalking.equals(NetUtil.PERSONALTALK)) {
    strTalking = new String(is.readLine());
    int vi = 0;
    for (vi = 0; vi < frm.dataProcessor.vecSelfInfo.size()
    && !((String)((Vector)frm.dataProcessor.vecSelfInfo.get(vi)).get(0)).equals(strTalking); vi++);
    if (vi == frm.dataProcessor.vecSelfInfo.size()) {
    os.println(NetUtil.NOTEXIST);
    os.flush();
    }
    else {
    Vector vec = (Vector)frm.dataProcessor.vecSelfInfo.get(vi);
    String strCall = new String(is.readLine());
    vec.setElementAt(NetUtil.CALLED, 1);
    vec.setElementAt(strCall, 2);
    int vi_c = 0;
    for (vi_c = 0; vi_c < frm.dataProcessor.vecSelfInfo.size()
    && !((String)((Vector)frm.dataProcessor.vecSelfInfo.get(vi_c)).get(0)).equals(strCall); vi_c++);
    if (vi_c == frm.dataProcessor.vecSelfInfo.size()) {
    os.println(NetUtil.NOTEXIST);
    os.flush();
    }
    Vector vec_c = (Vector)frm.dataProcessor.vecSelfInfo.get(vi_c);
    vec_c.setElementAt(NetUtil.CALLING, 1);
    vec_c.setElementAt(strTalking, 2);
    os.println(NetUtil.RESPONSE);
    os.flush();
    }
    flagEndProc = true;
    }
    // 存储新客户的信息
    else if (strTalking.equals(NetUtil.PERSONNAME)) {
    String strName = "";
    frm.isStart = true;
    strName = new String(is.readLine());
    if (strName != "\n") {
    frm.dataProcessor.vecP2PInfo.addElement(strName);
    Vector vec = new Vector();
    vec.addElement(strName);
    vec.addElement(NetUtil.IDLE);
    vec.addElement(NetUtil.NONE);
    vec.addElement(NetUtil.NORESPONSE);
    vec.addElement(NetUtil.NONCONTENT);
    frm.dataProcessor.vecSelfInfo.addElement(vec);
    }
    flagEndProc = true;
    }
    //私下交谈时,处理被叫方发送的应答请求
    else if (strTalking.equals(NetUtil.SETRESPONSE)) {
    String strResName = new String(is.readLine());
    int res = 0;
    for (res = 0; res < frm.dataProcessor.vecSelfInfo.size()
    &&!((String)((Vector)frm.dataProcessor.vecSelfInfo.get(res)).get(0)).equals(strResName); res++);
    Vector vecRes = (Vector)frm.dataProcessor.vecSelfInfo.get(res);
    vecRes.setElementAt(NetUtil.RESPONSING, 3);
    }
    //私下交谈时,处理主叫方发送的“开始交谈“请求
    else if (strTalking.equals(NetUtil.PERSONALTALKSTART)) {
    String strPerCallName = new String(is.readLine());
    String strPerTalkContent = new String(is.readLine());
    int pres = 0;
    for (pres = 0; pres < frm.dataProcessor.vecSelfInfo.size()
    &&!((String)((Vector)frm.dataProcessor.vecSelfInfo.get(pres)).get(0)).equals(strPerCallName);
    pres++);
    Vector vecPer = (Vector)frm.dataProcessor.vecSelfInfo.get(pres);
    vecPer.setElementAt(strPerTalkContent, 4);
    }
    else {
    if (!strTalking.equals("\n") && !strTalking.equals("")) {
    frm.dataProcessor.vecMainTalk.addElement(strTalking);
    strTalking += "\n";
    }
    flagEndProc = true;
    }}
    } catch (Exception io_e) {
    frm.statusBar.setText(io_e.getMessage());
    }
    if (flagEndProc)
    break;
    }}}

    以上程序实现了对客户请求的处理。客户登录后,就可以发言了(简化了客户身份验证)。客户可以在大厅发言(每位在线客户都能接收到该发言),也可以选择某一位客户进行私下交谈(只有被选择的客户能收到该信息)。限于篇幅的原因,只实现了这几个简单功能。其它功能可以参考着实现。

    客户信息,客户谈话内容等公共数据存放在CmDataProcessor的实例中。该实例是GUI窗口类的一个成员变量,在初始化时创建。CmDataProcessor中简化了对公共数据的处理,没有与后方的DBMS交互。因为本文重点是即时通信,所以没有实现与数据库交互。CmDataProcessor类中有三个主要成员变量.vecMainTalk用来存放客户的大厅谈话内容,vecSelfInfo存放每个在线客户的状态信息,vecP2Pinfo存放在线客户列表。需要说明的是,客户会话(ClientSession)这个类的parseRequest ()方法的实现细节不是本文的重点,因为其实现是根据应用的不同而不同的,尽管程序清单中给出了比较详细的注释。以下是类CmDataProcessor的实现代码(程序清单1。4。1):

    程序清单1。4。1

    public class CmDataProcessor {
      Vector vecMainTalk = new Vector();
      Vector vecSelfInfo = new Vector();
      Vector vecP2PInfo = new Vector();
      public CmDataProcessor() {
        vecMainTalk.addElement(NetUtil.WELCOME); //登录时的欢迎界面
      }
    }

    接口NetUtil和GUI用到的类的实现这里从略。到此,服务器端程序已实现。下面是客户端程序的实现。

    客户端程序的实现:客户端程序首先实现的是类Reciever。代码如下(程序清单2。1。1):

    程序清单2。1。1

    class Receiver extends Thread {
    PrintStream os;
    BufferedReader is;
    Socket clientSocket = null;
    public Receiver (Socket socket) {
    clientSocket = socket; //Socket实例是在applet初始化时创建的,而不能在线程内创建
    }
    public void run () {
    if (clientSocket == null) {
    return;
    }
    try {
    os = new PrintStream(clientSocket.getOutputStream());
    is = new BufferedReader(new InputStreamReader(clientSocket.getInputStream()));
    } catch (Exception se) {
    String err = se.getMessage() + "\n";
    }
    String strTalking;
    String strStart = "";
    strStart = NetUtil.CHATSTART + "\n";
    strStart += strUserID;
    os.println(strStart);
    os.flush();
    while (true) {
    try {
    while ((strTalking = new String(is.readLine())) != null) {
    if (strTalking.equals(NetUtil.PERSONINFO)) { //显示在线客户信息
    while ((strTalking = new String(is.readLine()))!= null) {
    if (strTalking.equals(NetUtil.DIVIDEDLINE)) {
    break;
    }
    if (!strTalking.equals("\n") && !strTalking.equals("")) {
    addUser(strTalking);
    }}}
    else if (strTalking.equals(NetUtil.ISCALLED)) {
    String strCallName = (String)is.readLine();
    strCallingName = strCallName;
    textNotify.setEnabled(true);
    textNotify.setText(strCallName + " is calling you!");
    }
    else if (strTalking.equals(NetUtil.RESPONSE)) {
    String strResponseName = (String)is.readLine();
    textNotify.setText("You are chatting with " + strResponseName);
    strCallingName = strResponseName;
    isTalkWith = true;
    choiceUser.addItem(strResponseName);
    }
    else if (strTalking.equals(NetUtil.PERSONALRECEIVE)) {
    String strWhoIs = (String)is.readLine();
    String strContent = (String)is.readLine();
    textTalkContent.append(strWhoIs + " speak to you: "+ strContent + "\n");
    }
    else {
    strTalking += "\n";
    textTalkContent.append(strTalking);
    }}
    } catch (Exception io_e) {
    System.out.println(io_e.getMessage());
    }}}}

    该类用来实现与服务器端数据同步。当有客户发言或者有客户和另一客户私下交谈时,该类将立即更新这些数据,在客户端显示出来。所以,该类在客户会话期间一直在后台运行。负责发送客户请求的类是RequestSender。RequestSender用成员方法chatAll ()和chatOne ()分别实现大厅发言和私下交谈,其实现可以参照以上程序。客户端applet的实现代码这里从略。至此,客户端程序也已完成。以下是客户端程序运行时的快照(图2。4。1和图2。4。2)。

    图2。4。1
    图2。4。1

    图2。4。2
    图2。4。2

    5.系统扩展及补充说明
    以上实例由于篇幅原因只实现很少一部分功能,但能体现该即时通信系统的总体设计思想,而且很容易实现功能扩展。例如,可以实现公共数据处理类(CmDataProcessor)与DBMS的交互,实现数据的持久化(persistence);可以对applet实现数字签名来加大它对客户机的访问权限,从而实现文件的传输,实现多媒体交互;可以实现applet与servlet的对话,将比较复杂的业务逻辑交给应用服务器中的EJB来处理等。由于本人水平有限,难免有错误之处,欢迎批评指正。

    关于作者
    杨健,中南工业大学计算机科学与技术专业硕士研究生,参与过大型MIS系统开发,网站建设等,现在正致力于Java技术的研究。Email: ytjcopy@263.net


    服务器端的管理工具,如对数据进行统计,紧急情况的处理等。

    服务器端类的设计(图2。3。2和图2。3。3):

    公共数据处理类CmDataProcessor(Common Data Processor):该类包含客户所共有的数据,以及如何对这些数据进行处理。

    端口监听类PortListener(Port Listener):该类实现了java. lang. Runnable接口,从服务器程序初始化完成后一直运行。由于目前JDK只支持同步通信,在没有客户请求时,该线程处于等待状态;一旦有客户请求到来,便继续执行。这时服务器程序可以通过java. net. ServerSocket. accept()方法获得客户端请求的java. net. Socket对象。然后用这个Socket对象为参数构造一个新的线程:ClientSession的实例(类ClientSession以下将作介绍)。然后在ClientSession实例中用该Socket对象构造一个输出流java. io. PrintStream和一个输入流java. io. BufferedReader,以后,每个客户就可以通过这一对输入输出流与服务器交互了。应该注意的是,ServerSocket 对象并不是在该对象内创建的,而是在服务器程序初始化时创建的。因为socket是进程间的通信,在线程中创建将会失败。客户端程序也是如此。

    客户会话类ClientSession(Client Session):该类继承自java. lang. Thread类,由PortListener创建。一般的,每一个在线客户都对应一个ClientSession的实例。该类用parseRequest()方法解析客户发来的请求,进行相应处理。该线程在客户会话期间一直运行,通过I/O流读取和发送数据(I/O流即从PortListener监听线程获得的java. net. Socket对象而创建的java. io. PrintStream和java. io. BufferedReader实例),直到客户退出才撤销。该类和类ClientSession一样都实现了java. lang. Runnable接口,故都有一个run()方法。该方法的结束标志着该线程将结束。

    服务器管理类 ServerManager(Server Manager):管理服务器。拥有管理权限的客户(管理员)可以远程操作服务器程序,包括运行、停止服务器,广播通知,给指定客户发送消息等特权操作。

    图2。3。2
    图2。3。2

    图2。3。3

  3. 服务器端类的设计(图2。3。2和图2。3。3):

    公共数据处理类CmDataProcessor(Common Data Processor):该类包含客户所共有的数据,以及如何对这些数据进行处理。

    端口监听类PortListener(Port Listener):该类实现了java. lang. Runnable接口,从服务器程序初始化完成后一直运行。由于目前JDK只支持同步通信,在没有客户请求时,该线程处于等待状态;一旦有客户请求到来,便继续执行。这时服务器程序可以通过java. net. ServerSocket. accept()方法获得客户端请求的java. net. Socket对象。然后用这个Socket对象为参数构造一个新的线程:ClientSession的实例(类ClientSession以下将作介绍)。然后在ClientSession实例中用该Socket对象构造一个输出流java. io. PrintStream和一个输入流java. io. BufferedReader,以后,每个客户就可以通过这一对输入输出流与服务器交互了。应该注意的是,ServerSocket 对象并不是在该对象内创建的,而是在服务器程序初始化时创建的。因为socket是进程间的通信,在线程中创建将会失败。客户端程序也是如此。

    客户会话类ClientSession(Client Session):该类继承自java. lang. Thread类,由PortListener创建。一般的,每一个在线客户都对应一个ClientSession的实例。该类用parseRequest()方法解析客户发来的请求,进行相应处理。该线程在客户会话期间一直运行,通过I/O流读取和发送数据(I/O流即从PortListener监听线程获得的java. net. Socket对象而创建的java. io. PrintStream和java. io. BufferedReader实例),直到客户退出才撤销。该类和类ClientSession一样都实现了java. lang. Runnable接口,故都有一个run()方法。该方法的结束标志着该线程将结束。

    服务器管理类 ServerManager(Server Manager):管理服务器。拥有管理权限的客户(管理员)可以远程操作服务器程序,包括运行、停止服务器,广播通知,给指定客户发送消息等特权操作。

    图2。3。2
    图2。3。2

    图2。3。3

  4. 客户端程序设计(图2。3。4)

    客户端完成的功能是:建立与服务器的连接;向服务器发送功能请求,接收来自服务器的信息,完成与主机或其他客户交互;断开与服务器的连接。客户端程序相对服务器端程序来说属于LightWeight(轻量级)。这是由本系统的自身特点决定的。所以,对客户端程序抽象如下:

    1. 客户请求发送器:负责功能请求的发送。如登录请求等。
    2. 服务器信息接收器:负责接收来自服务器端的信息。如请求处理结果等。


    客户端类的设计:
    请求发送器(RequestSender):该类发送客户端的功能请求。客户通过客户端用户界面提交要执行的操作,然后由该类将客户提交的信息封装成服务器端程序可以理解的功能请求发送出去。

    信息接收器(Receiver):该类接收来服务器端的信息。这些信息可以是客户请求的处理结果,也可以是服务器端的广播通知。为保证实时性,该类实现了java. lang. Runnable接口。在客户会话期间,该类将一直运行,实时的将来自服务器端的信息反馈给客户。该类接收信息后,应该对该信息做相应处理。如通知客户已登录成功等。这些操作都将在run()方法中实现。

    图2。3。4
    图2。3。4

    4. 实现
    以上的系统设计是一个即时通信系统的总体框架,根据实际情况,可以添加或者修改。下文就以“远程会议系统“为例来实例化这样一个通信系统。

    我们知道,远程会议系统有几个方面的特点:实时交互;准确传输信息;多客户等。所以,完全可以用该系统框架来实现(这里只给出核心代码)。

    首先我们来实现服务器端程序。为了便于对服务器程序的管理,服务器端程序采用了GUI界面。在该程序初始化时应该实现对可用端口的监听(程序清单1。1)。

    程序清单1。1。1

        try {
              socket = new ServerSocket(NetUtil.SLISTENPORT);
            }
    catch (Exception se)
        {
                statusBar.setText(se.getMessage());
            }

    其中,NetUtil.SLISTENPORT是服务器的一个可用端口,可以根据实际情况确定。NetUtil是一个接口,其中包含了该系统用到的各类常数。NetUtil.SLISTENPORT就是NetUtil中的一个整型常数。如果端口监听抛出异常,GUI中的statusBar将给出提示。监听成功后可以启动监听线程了(程序清单1。1。2)。

    程序清单1。1。2

    listenThread=new Thread (new ListenThread ());
    listenThread.start();
    

    以上程序中listenThread是一个Thread实例,用来操作监听线程。监听线程实现如下(程序清单1。2。1):

    程序清单1。2。1

       public class PortListener implements Runnable{
        ServerSocket socket; //服务器监听端口
        FrmServer frm; //FrmServer为GUI窗口类
        ClientSession cs; //客户端会话线程
        PortListener (FrmServer frm,ServerSocket socket) {
          this.frm=frm;
          this.socket=socket;
        }
          public void run () {
            int count = 0;
            if (socket == null) {
                frm.statusBar.setText("socket failed!");
                return;
            }
            while (true) {
                try {
                    Socket clientSocket;
                    clientSocket = socket.accept(); //取得客户请求Socket
                    //用取得的Socket构造输入输出流
                    PrintStream os = new PrintStream(new
                    BufferedOutputStream(clientSocket.getOutputStream(),
                    1024), false);
                    BufferedReader is = new BufferedReader(new
                    InputStreamReader(clientSocket.getInputStream()));
                    //创建客户会话线程
                    cs = new ClientSession(this.frm);
                    cs.is = is;
                    cs.os = os;
                    cs.clientSock = clientSocket;
                    cs.start();
                    } catch (Exception e_socket) {
                    frm.statusBar.setText(e_socket.getMessage());
                }
            }
        }
    }

    监听线程一直在后台运行。当有客户请求到来时,监听线程创建与该客户进行会话的ClientSession实例。这时,监听线程会等待另外客户请求的到来,然后又创建会话线程,如此循环下去。客户会话则通过会话线程进行。客户会话线程主要的工作就是怎样处理客户请求。ClientSession. parseRequest ()就是处理客户请求的。这个方法的内容应该根据实际应用的需要来确定。这里只实现了一些很简单的功能。如会议大厅发言,私下交谈等。ClientSession的实现代码见程序清单1。3。1。

    程序清单1。3。1

    public class ClientSession extends Thread {
        FrmServer frm; //GUI窗口类
        BufferedReader is = null; //输入流
        PrintStream os = null; //输出流
        Socket clientSock = null; //客户请求Socket
        int curContent = 0;
        public ClientSession (FrmServer frm) {
          this.frm=frm;
        }
        public void run () {
            parseRequest(); //处理客户请求
        }
        public void destroy () {
            try {
                clientSock.close();
                is.close();
                os.close();
            } catch (Exception cs_e) {}
        }
        //客户请求处理方法,只实现了大厅发言及私下交谈
        private void parseRequest () {
            String strTalking;
            String strUserID;
            String strMain = "";
            String strPerson = NetUtil.PERSONINFO + "\n";
            boolean flagEndProc = false;
            while (true) {
                try {
                    while ((strTalking = new String(is.readLine())) != null) {
                        if(strTalking.equals(NetUtil.CHATSTART)){ //客户会话开始
                            strUserID = new String(is.readLine());
                            // 将所有谈话内容发送给刚登录的客户
                            for (int i = 0; i < frm.dataProcessor.vecMainTalk.size(); i++) {
                                strMain += (String)frm.dataProcessor.vecMainTalk.get(i);
                                strMain += "\n";
                            }
                            curContent = frm.dataProcessor.vecMainTalk.size();
                            os.println(strMain);
                            os.flush();
                            for (int j = 0; j < frm.dataProcessor.vecP2PInfo.size(); j++) {
                                strPerson += (String)frm.dataProcessor.vecP2PInfo.get(j);
                                strPerson += "\n";
                            }
                            os.println(strPerson);
    os.flush(); //将所有在线用户名单发给新加入的客户
    os.println(NetUtil.DIVIDEDLINE);
    os.flush();
    while (true) {
    this.sleep(1000);
    String strContent = "";
    //如果有人发言,则把发言发给在线客户
    if (frm.dataProcessor.vecMainTalk.size() > curContent) {
    for (int ci = curContent; ci <frm.dataProcessor.vecMainTalk.size(); ci++) {
    strContent +=(String)frm.dataProcessor.vecMainTalk.get(ci);
    strContent += "\n";
    }
    curContent = frm.dataProcessor.vecMainTalk.size();
    os.println(strContent);
    os.flush();
    }
    //如果有人私下交谈,则把交谈的内容发给交谈的另一方
    if (strUserID != null) {
    int nvi = 0;
    for (nvi = 0; nvi <
    frm.dataProcessor.vecSelfInfo.size()&& !((String)((Vector)frm.dataProcessor.vecSelfInfo.get
    (nvi)).get(0)).equals(strUserID); nvi++);
    if (nvi < frm.dataProcessor.vecSelfInfo.size()) {
    Vector vecTalk =(Vector)frm.dataProcessor.vecSelfInfo.get(nvi);
    if ((String)vecTalk.get(1)).equals(NetUtil.CALLED)){
    String strCallRes = NetUtil.ISCALLED+ "\n";
    String strCallTemp = (String)vecTalk.get(2);
    strCallRes += strCallTemp;
    os.println(strCallRes);
    os.flush();
    }
    else if(((String)vecTalk.get(1)).equals(NetUtil.CALLING)){
    if(((String)vecTalk.get(3)).equals(NetUtil.RESPONSING)){ //一客户呼叫另一客户并有了回应
    String strResponsing =NetUtil.RESPONSE + "\n";
    String strResponsingT =(String)vecTalk.get(2);
    strResponsing += strResponsingT;
    os.println(strResponsing);
    os.flush();
    //设置客户“正在私下交谈”状态
    vecTalk.setElementAt(NetUtil.CHATTING, 1);
    String strOther = (String)vecTalk.get(2);
    int setvi = 0;
    for (setvi = 0; setvi <frm.dataProcessor.vecSelfInfo.size()
    && !((String)((Vector)frm.dataProcessor.vecSelfInfo.get(setvi)).get(0)).equals(strOther); setvi++);
    Vector vecOther = (Vector)frm.dataProcessor.vecSelfInfo.get(setvi);
    vecOther.setElementAt(NetUtil.CHATTING,1);
    }}
    else if (((String)vecTalk.get(1)).equals(NetUtil.CHATTING)) {
    String strCurContent = (String)vecTalk.get(4);
    if (!strCurContent.equals(NetUtil.NONCONTENT)) {
    String strToWho = vecTalk.get(2)+ "\n";
    String strPerRes = NetUtil.PERSONALRECEIVE+ "\n";
    strPerRes += strToWho;
    strPerRes += strCurContent;
    os.println(strPerRes);
    os.flush();
    vecTalk.setElementAt(NetUtil.NONCONTENT,4);
    }}}}}}
    // 处理客户发来与另一客户私下交谈的请求
    else if (strTalking.equals(NetUtil.PERSONALTALK)) {
    strTalking = new String(is.readLine());
    int vi = 0;
    for (vi = 0; vi < frm.dataProcessor.vecSelfInfo.size()
    && !((String)((Vector)frm.dataProcessor.vecSelfInfo.get(vi)).get(0)).equals(strTalking); vi++);
    if (vi == frm.dataProcessor.vecSelfInfo.size()) {
    os.println(NetUtil.NOTEXIST);
    os.flush();
    }
    else {
    Vector vec = (Vector)frm.dataProcessor.vecSelfInfo.get(vi);
    String strCall = new String(is.readLine());
    vec.setElementAt(NetUtil.CALLED, 1);
    vec.setElementAt(strCall, 2);
    int vi_c = 0;
    for (vi_c = 0; vi_c < frm.dataProcessor.vecSelfInfo.size()
    && !((String)((Vector)frm.dataProcessor.vecSelfInfo.get(vi_c)).get(0)).equals(strCall); vi_c++);
    if (vi_c == frm.dataProcessor.vecSelfInfo.size()) {
    os.println(NetUtil.NOTEXIST);
    os.flush();
    }
    Vector vec_c = (Vector)frm.dataProcessor.vecSelfInfo.get(vi_c);
    vec_c.setElementAt(NetUtil.CALLING, 1);
    vec_c.setElementAt(strTalking, 2);
    os.println(NetUtil.RESPONSE);
    os.flush();
    }
    flagEndProc = true;
    }
    // 存储新客户的信息
    else if (strTalking.equals(NetUtil.PERSONNAME)) {
    String strName = "";
    frm.isStart = true;
    strName = new String(is.readLine());
    if (strName != "\n") {
    frm.dataProcessor.vecP2PInfo.addElement(strName);
    Vector vec = new Vector();
    vec.addElement(strName);
    vec.addElement(NetUtil.IDLE);
    vec.addElement(NetUtil.NONE);
    vec.addElement(NetUtil.NORESPONSE);
    vec.addElement(NetUtil.NONCONTENT);
    frm.dataProcessor.vecSelfInfo.addElement(vec);
    }
    flagEndProc = true;
    }
    //私下交谈时,处理被叫方发送的应答请求
    else if (strTalking.equals(NetUtil.SETRESPONSE)) {
    String strResName = new String(is.readLine());
    int res = 0;
    for (res = 0; res < frm.dataProcessor.vecSelfInfo.size()
    &&!((String)((Vector)frm.dataProcessor.vecSelfInfo.get(res)).get(0)).equals(strResName); res++);
    Vector vecRes = (Vector)frm.dataProcessor.vecSelfInfo.get(res);
    vecRes.setElementAt(NetUtil.RESPONSING, 3);
    }
    //私下交谈时,处理主叫方发送的“开始交谈“请求
    else if (strTalking.equals(NetUtil.PERSONALTALKSTART)) {
    String strPerCallName = new String(is.readLine());
    String strPerTalkContent = new String(is.readLine());
    int pres = 0;
    for (pres = 0; pres < frm.dataProcessor.vecSelfInfo.size()
    &&!((String)((Vector)frm.dataProcessor.vecSelfInfo.get(pres)).get(0)).equals(strPerCallName);
    pres++);
    Vector vecPer = (Vector)frm.dataProcessor.vecSelfInfo.get(pres);
    vecPer.setElementAt(strPerTalkContent, 4);
    }
    else {
    if (!strTalking.equals("\n") && !strTalking.equals("")) {
    frm.dataProcessor.vecMainTalk.addElement(strTalking);
    strTalking += "\n";
    }
    flagEndProc = true;
    }}
    } catch (Exception io_e) {
    frm.statusBar.setText(io_e.getMessage());
    }
    if (flagEndProc)
    break;
    }}}

    以上程序实现了对客户请求的处理。客户登录后,就可以发言了(简化了客户身份验证)。客户可以在大厅发言(每位在线客户都能接收到该发言),也可以选择某一位客户进行私下交谈(只有被选择的客户能收到该信息)。限于篇幅的原因,只实现了这几个简单功能。其它功能可以参考着实现。

    客户信息,客户谈话内容等公共数据存放在CmDataProcessor的实例中。该实例是GUI窗口类的一个成员变量,在初始化时创建。CmDataProcessor中简化了对公共数据的处理,没有与后方的DBMS交互。因为本文重点是即时通信,所以没有实现与数据库交互。CmDataProcessor类中有三个主要成员变量.vecMainTalk用来存放客户的大厅谈话内容,vecSelfInfo存放每个在线客户的状态信息,vecP2Pinfo存放在线客户列表。需要说明的是,客户会话(ClientSession)这个类的parseRequest ()方法的实现细节不是本文的重点,因为其实现是根据应用的不同而不同的,尽管程序清单中给出了比较详细的注释。以下是类CmDataProcessor的实现代码(程序清单1。4。1):

    程序清单1。4。1

    public class CmDataProcessor {
      Vector vecMainTalk = new Vector();
      Vector vecSelfInfo = new Vector();
      Vector vecP2PInfo = new Vector();
      public CmDataProcessor() {
        vecMainTalk.addElement(NetUtil.WELCOME); //登录时的欢迎界面
      }
    }

    接口NetUtil和GUI用到的类的实现这里从略。到此,服务器端程序已实现。下面是客户端程序的实现。

    客户端程序的实现:客户端程序首先实现的是类Reciever。代码如下(程序清单2。1。1):

    程序清单2。1。1

    class Receiver extends Thread {
    PrintStream os;
    BufferedReader is;
    Socket clientSocket = null;
    public Receiver (Socket socket) {
    clientSocket = socket; //Socket实例是在applet初始化时创建的,而不能在线程内创建
    }
    public void run () {
    if (clientSocket == null) {
    return;
    }
    try {
    os = new PrintStream(clientSocket.getOutputStream());
    is = new BufferedReader(new InputStreamReader(clientSocket.getInputStream()));
    } catch (Exception se) {
    String err = se.getMessage() + "\n";
    }
    String strTalking;
    String strStart = "";
    strStart = NetUtil.CHATSTART + "\n";
    strStart += strUserID;
    os.println(strStart);
    os.flush();
    while (true) {
    try {
    while ((strTalking = new String(is.readLine())) != null) {
    if (strTalking.equals(NetUtil.PERSONINFO)) { //显示在线客户信息
    while ((strTalking = new String(is.readLine()))!= null) {
    if (strTalking.equals(NetUtil.DIVIDEDLINE)) {
    break;
    }
    if (!strTalking.equals("\n") && !strTalking.equals("")) {
    addUser(strTalking);
    }}}
    else if (strTalking.equals(NetUtil.ISCALLED)) {
    String strCallName = (String)is.readLine();
    strCallingName = strCallName;
    textNotify.setEnabled(true);
    textNotify.setText(strCallName + " is calling you!");
    }
    else if (strTalking.equals(NetUtil.RESPONSE)) {
    String strResponseName = (String)is.readLine();
    textNotify.setText("You are chatting with " + strResponseName);
    strCallingName = strResponseName;
    isTalkWith = true;
    choiceUser.addItem(strResponseName);
    }
    else if (strTalking.equals(NetUtil.PERSONALRECEIVE)) {
    String strWhoIs = (String)is.readLine();
    String strContent = (String)is.readLine();
    textTalkContent.append(strWhoIs + " speak to you: "+ strContent + "\n");
    }
    else {
    strTalking += "\n";
    textTalkContent.append(strTalking);
    }}
    } catch (Exception io_e) {
    System.out.println(io_e.getMessage());
    }}}}

    该类用来实现与服务器端数据同步。当有客户发言或者有客户和另一客户私下交谈时,该类将立即更新这些数据,在客户端显示出来。所以,该类在客户会话期间一直在后台运行。负责发送客户请求的类是RequestSender。RequestSender用成员方法chatAll ()和chatOne ()分别实现大厅发言和私下交谈,其实现可以参照以上程序。客户端applet的实现代码这里从略。至此,客户端程序也已完成。以下是客户端程序运行时的快照(图2。4。1和图2。4。2)。

    图2。4。1
    图2。4。1

    图2。4。2
    图2。4。2

    5.系统扩展及补充说明
    以上实例由于篇幅原因只实现很少一部分功能,但能体现该即时通信系统的总体设计思想,而且很容易实现功能扩展。例如,可以实现公共数据处理类(CmDataProcessor)与DBMS的交互,实现数据的持久化(persistence);可以对applet实现数字签名来加大它对客户机的访问权限,从而实现文件的传输,实现多媒体交互;可以实现applet与servlet的对话,将比较复杂的业务逻辑交给应用服务器中的EJB来处理等。由于本人水平有限,难免有错误之处,欢迎批评指正。

    关于作者
    杨健,中南工业大学计算机科学与技术专业硕士研究生,参与过大型MIS系统开发,网站建设等,现在正致力于Java技术的研究。Email: ytjcopy@263.net


    服务器端的管理工具,如对数据进行统计,紧急情况的处理等。

    服务器端类的设计(图2。3。2和图2。3。3):

    公共数据处理类CmDataProcessor(Common Data Processor):该类包含客户所共有的数据,以及如何对这些数据进行处理。

    端口监听类PortListener(Port Listener):该类实现了java. lang. Runnable接口,从服务器程序初始化完成后一直运行。由于目前JDK只支持同步通信,在没有客户请求时,该线程处于等待状态;一旦有客户请求到来,便继续执行。这时服务器程序可以通过java. net. ServerSocket. accept()方法获得客户端请求的java. net. Socket对象。然后用这个Socket对象为参数构造一个新的线程:ClientSession的实例(类ClientSession以下将作介绍)。然后在ClientSession实例中用该Socket对象构造一个输出流java. io. PrintStream和一个输入流java. io. BufferedReader,以后,每个客户就可以通过这一对输入输出流与服务器交互了。应该注意的是,ServerSocket 对象并不是在该对象内创建的,而是在服务器程序初始化时创建的。因为socket是进程间的通信,在线程中创建将会失败。客户端程序也是如此。

    客户会话类ClientSession(Client Session):该类继承自java. lang. Thread类,由PortListener创建。一般的,每一个在线客户都对应一个ClientSession的实例。该类用parseRequest()方法解析客户发来的请求,进行相应处理。该线程在客户会话期间一直运行,通过I/O流读取和发送数据(I/O流即从PortListener监听线程获得的java. net. Socket对象而创建的java. io. PrintStream和java. io. BufferedReader实例),直到客户退出才撤销。该类和类ClientSession一样都实现了java. lang. Runnable接口,故都有一个run()方法。该方法的结束标志着该线程将结束。

    服务器管理类 ServerManager(Server Manager):管理服务器。拥有管理权限的客户(管理员)可以远程操作服务器程序,包括运行、停止服务器,广播通知,给指定客户发送消息等特权操作。

    图2。3。2
    图2。3。2

    图2。3。3

  5. 客户端程序设计(图2。3。4)

    客户端完成的功能是:建立与服务器的连接;向服务器发送功能请求,接收来自服务器的信息,完成与主机或其他客户交互;断开与服务器的连接。客户端程序相对服务器端程序来说属于LightWeight(轻量级)。这是由本系统的自身特点决定的。所以,对客户端程序抽象如下:

    1. 客户请求发送器:负责功能请求的发送。如登录请求等。
    2. 服务器信息接收器:负责接收来自服务器端的信息。如请求处理结果等。


    客户端类的设计:
    请求发送器(RequestSender):该类发送客户端的功能请求。客户通过客户端用户界面提交要执行的操作,然后由该类将客户提交的信息封装成服务器端程序可以理解的功能请求发送出去。

    信息接收器(Receiver):该类接收来服务器端的信息。这些信息可以是客户请求的处理结果,也可以是服务器端的广播通知。为保证实时性,该类实现了java. lang. Runnable接口。在客户会话期间,该类将一直运行,实时的将来自服务器端的信息反馈给客户。该类接收信息后,应该对该信息做相应处理。如通知客户已登录成功等。这些操作都将在run()方法中实现。

    图2。3。4
    图2。3。4

4. 实现
以上的系统设计是一个即时通信系统的总体框架,根据实际情况,可以添加或者修改。下文就以“远程会议系统“为例来实例化这样一个通信系统。

我们知道,远程会议系统有几个方面的特点:实时交互;准确传输信息;多客户等。所以,完全可以用该系统框架来实现(这里只给出核心代码)。

首先我们来实现服务器端程序。为了便于对服务器程序的管理,服务器端程序采用了GUI界面。在该程序初始化时应该实现对可用端口的监听(程序清单1。1)。

程序清单1。1。1

    try {
          socket = new ServerSocket(NetUtil.SLISTENPORT);
        }
catch (Exception se)
    {
            statusBar.setText(se.getMessage());
        }

其中,NetUtil.SLISTENPORT是服务器的一个可用端口,可以根据实际情况确定。NetUtil是一个接口,其中包含了该系统用到的各类常数。NetUtil.SLISTENPORT就是NetUtil中的一个整型常数。如果端口监听抛出异常,GUI中的statusBar将给出提示。监听成功后可以启动监听线程了(程序清单1。1。2)。

程序清单1。1。2

listenThread=new Thread (new ListenThread ());
listenThread.start();

以上程序中listenThread是一个Thread实例,用来操作监听线程。监听线程实现如下(程序清单1。2。1):

程序清单1。2。1

   public class PortListener implements Runnable{
    ServerSocket socket; //服务器监听端口
    FrmServer frm; //FrmServer为GUI窗口类
    ClientSession cs; //客户端会话线程
    PortListener (FrmServer frm,ServerSocket socket) {
      this.frm=frm;
      this.socket=socket;
    }
      public void run () {
        int count = 0;
        if (socket == null) {
            frm.statusBar.setText("socket failed!");
            return;
        }
        while (true) {
            try {
                Socket clientSocket;
                clientSocket = socket.accept(); //取得客户请求Socket
                //用取得的Socket构造输入输出流
                PrintStream os = new PrintStream(new
                BufferedOutputStream(clientSocket.getOutputStream(),
                1024), false);
                BufferedReader is = new BufferedReader(new
                InputStreamReader(clientSocket.getInputStream()));
                //创建客户会话线程
                cs = new ClientSession(this.frm);
                cs.is = is;
                cs.os = os;
                cs.clientSock = clientSocket;
                cs.start();
                } catch (Exception e_socket) {
                frm.statusBar.setText(e_socket.getMessage());
            }
        }
    }
}

监听线程一直在后台运行。当有客户请求到来时,监听线程创建与该客户进行会话的ClientSession实例。这时,监听线程会等待另外客户请求的到来,然后又创建会话线程,如此循环下去。客户会话则通过会话线程进行。客户会话线程主要的工作就是怎样处理客户请求。ClientSession. parseRequest ()就是处理客户请求的。这个方法的内容应该根据实际应用的需要来确定。这里只实现了一些很简单的功能。如会议大厅发言,私下交谈等。ClientSession的实现代码见程序清单1。3。1。

程序清单1。3。1

public class ClientSession extends Thread {
    FrmServer frm; //GUI窗口类
    BufferedReader is = null; //输入流
    PrintStream os = null; //输出流
    Socket clientSock = null; //客户请求Socket
    int curContent = 0;
    public ClientSession (FrmServer frm) {
      this.frm=frm;
    }
    public void run () {
        parseRequest(); //处理客户请求
    }
    public void destroy () {
        try {
            clientSock.close();
            is.close();
            os.close();
        } catch (Exception cs_e) {}
    }
    //客户请求处理方法,只实现了大厅发言及私下交谈
    private void parseRequest () {
        String strTalking;
        String strUserID;
        String strMain = "";
        String strPerson = NetUtil.PERSONINFO + "\n";
        boolean flagEndProc = false;
        while (true) {
            try {
                while ((strTalking = new String(is.readLine())) != null) {
                    if(strTalking.equals(NetUtil.CHATSTART)){ //客户会话开始
                        strUserID = new String(is.readLine());
                        // 将所有谈话内容发送给刚登录的客户
                        for (int i = 0; i < frm.dataProcessor.vecMainTalk.size(); i++) {
                            strMain += (String)frm.dataProcessor.vecMainTalk.get(i);
                            strMain += "\n";
                        }
                        curContent = frm.dataProcessor.vecMainTalk.size();
                        os.println(strMain);
                        os.flush();
                        for (int j = 0; j < frm.dataProcessor.vecP2PInfo.size(); j++) {
                            strPerson += (String)frm.dataProcessor.vecP2PInfo.get(j);
                            strPerson += "\n";
                        }
                        os.println(strPerson);
os.flush(); //将所有在线用户名单发给新加入的客户
os.println(NetUtil.DIVIDEDLINE);
os.flush();
while (true) {
this.sleep(1000);
String strContent = "";
//如果有人发言,则把发言发给在线客户
if (frm.dataProcessor.vecMainTalk.size() > curContent) {
for (int ci = curContent; ci <frm.dataProcessor.vecMainTalk.size(); ci++) {
strContent +=(String)frm.dataProcessor.vecMainTalk.get(ci);
strContent += "\n";
}
curContent = frm.dataProcessor.vecMainTalk.size();
os.println(strContent);
os.flush();
}
//如果有人私下交谈,则把交谈的内容发给交谈的另一方
if (strUserID != null) {
int nvi = 0;
for (nvi = 0; nvi <
frm.dataProcessor.vecSelfInfo.size()&& !((String)((Vector)frm.dataProcessor.vecSelfInfo.get
(nvi)).get(0)).equals(strUserID); nvi++);
if (nvi < frm.dataProcessor.vecSelfInfo.size()) {
Vector vecTalk =(Vector)frm.dataProcessor.vecSelfInfo.get(nvi);
if ((String)vecTalk.get(1)).equals(NetUtil.CALLED)){
String strCallRes = NetUtil.ISCALLED+ "\n";
String strCallTemp = (String)vecTalk.get(2);
strCallRes += strCallTemp;
os.println(strCallRes);
os.flush();
}
else if(((String)vecTalk.get(1)).equals(NetUtil.CALLING)){
if(((String)vecTalk.get(3)).equals(NetUtil.RESPONSING)){ //一客户呼叫另一客户并有了回应
String strResponsing =NetUtil.RESPONSE + "\n";
String strResponsingT =(String)vecTalk.get(2);
strResponsing += strResponsingT;
os.println(strResponsing);
os.flush();
//设置客户“正在私下交谈”状态
vecTalk.setElementAt(NetUtil.CHATTING, 1);
String strOther = (String)vecTalk.get(2);
int setvi = 0;
for (setvi = 0; setvi <frm.dataProcessor.vecSelfInfo.size()
&& !((String)((Vector)frm.dataProcessor.vecSelfInfo.get(setvi)).get(0)).equals(strOther); setvi++);
Vector vecOther = (Vector)frm.dataProcessor.vecSelfInfo.get(setvi);
vecOther.setElementAt(NetUtil.CHATTING,1);
}}
else if (((String)vecTalk.get(1)).equals(NetUtil.CHATTING)) {
String strCurContent = (String)vecTalk.get(4);
if (!strCurContent.equals(NetUtil.NONCONTENT)) {
String strToWho = vecTalk.get(2)+ "\n";
String strPerRes = NetUtil.PERSONALRECEIVE+ "\n";
strPerRes += strToWho;
strPerRes += strCurContent;
os.println(strPerRes);
os.flush();
vecTalk.setElementAt(NetUtil.NONCONTENT,4);
}}}}}}
// 处理客户发来与另一客户私下交谈的请求
else if (strTalking.equals(NetUtil.PERSONALTALK)) {
strTalking = new String(is.readLine());
int vi = 0;
for (vi = 0; vi < frm.dataProcessor.vecSelfInfo.size()
&& !((String)((Vector)frm.dataProcessor.vecSelfInfo.get(vi)).get(0)).equals(strTalking); vi++);
if (vi == frm.dataProcessor.vecSelfInfo.size()) {
os.println(NetUtil.NOTEXIST);
os.flush();
}
else {
Vector vec = (Vector)frm.dataProcessor.vecSelfInfo.get(vi);
String strCall = new String(is.readLine());
vec.setElementAt(NetUtil.CALLED, 1);
vec.setElementAt(strCall, 2);
int vi_c = 0;
for (vi_c = 0; vi_c < frm.dataProcessor.vecSelfInfo.size()
&& !((String)((Vector)frm.dataProcessor.vecSelfInfo.get(vi_c)).get(0)).equals(strCall); vi_c++);
if (vi_c == frm.dataProcessor.vecSelfInfo.size()) {
os.println(NetUtil.NOTEXIST);
os.flush();
}
Vector vec_c = (Vector)frm.dataProcessor.vecSelfInfo.get(vi_c);
vec_c.setElementAt(NetUtil.CALLING, 1);
vec_c.setElementAt(strTalking, 2);
os.println(NetUtil.RESPONSE);
os.flush();
}
flagEndProc = true;
}
// 存储新客户的信息
else if (strTalking.equals(NetUtil.PERSONNAME)) {
String strName = "";
frm.isStart = true;
strName = new String(is.readLine());
if (strName != "\n") {
frm.dataProcessor.vecP2PInfo.addElement(strName);
Vector vec = new Vector();
vec.addElement(strName);
vec.addElement(NetUtil.IDLE);
vec.addElement(NetUtil.NONE);
vec.addElement(NetUtil.NORESPONSE);
vec.addElement(NetUtil.NONCONTENT);
frm.dataProcessor.vecSelfInfo.addElement(vec);
}
flagEndProc = true;
}
//私下交谈时,处理被叫方发送的应答请求
else if (strTalking.equals(NetUtil.SETRESPONSE)) {
String strResName = new String(is.readLine());
int res = 0;
for (res = 0; res < frm.dataProcessor.vecSelfInfo.size()
&&!((String)((Vector)frm.dataProcessor.vecSelfInfo.get(res)).get(0)).equals(strResName); res++);
Vector vecRes = (Vector)frm.dataProcessor.vecSelfInfo.get(res);
vecRes.setElementAt(NetUtil.RESPONSING, 3);
}
//私下交谈时,处理主叫方发送的“开始交谈“请求
else if (strTalking.equals(NetUtil.PERSONALTALKSTART)) {
String strPerCallName = new String(is.readLine());
String strPerTalkContent = new String(is.readLine());
int pres = 0;
for (pres = 0; pres < frm.dataProcessor.vecSelfInfo.size()
&&!((String)((Vector)frm.dataProcessor.vecSelfInfo.get(pres)).get(0)).equals(strPerCallName);
pres++);
Vector vecPer = (Vector)frm.dataProcessor.vecSelfInfo.get(pres);
vecPer.setElementAt(strPerTalkContent, 4);
}
else {
if (!strTalking.equals("\n") && !strTalking.equals("")) {
frm.dataProcessor.vecMainTalk.addElement(strTalking);
strTalking += "\n";
}
flagEndProc = true;
}}
} catch (Exception io_e) {
frm.statusBar.setText(io_e.getMessage());
}
if (flagEndProc)
break;
}}}

以上程序实现了对客户请求的处理。客户登录后,就可以发言了(简化了客户身份验证)。客户可以在大厅发言(每位在线客户都能接收到该发言),也可以选择某一位客户进行私下交谈(只有被选择的客户能收到该信息)。限于篇幅的原因,只实现了这几个简单功能。其它功能可以参考着实现。

客户信息,客户谈话内容等公共数据存放在CmDataProcessor的实例中。该实例是GUI窗口类的一个成员变量,在初始化时创建。CmDataProcessor中简化了对公共数据的处理,没有与后方的DBMS交互。因为本文重点是即时通信,所以没有实现与数据库交互。CmDataProcessor类中有三个主要成员变量.vecMainTalk用来存放客户的大厅谈话内容,vecSelfInfo存放每个在线客户的状态信息,vecP2Pinfo存放在线客户列表。需要说明的是,客户会话(ClientSession)这个类的parseRequest ()方法的实现细节不是本文的重点,因为其实现是根据应用的不同而不同的,尽管程序清单中给出了比较详细的注释。以下是类CmDataProcessor的实现代码(程序清单1。4。1):

程序清单1。4。1

public class CmDataProcessor {
  Vector vecMainTalk = new Vector();
  Vector vecSelfInfo = new Vector();
  Vector vecP2PInfo = new Vector();
  public CmDataProcessor() {
    vecMainTalk.addElement(NetUtil.WELCOME); //登录时的欢迎界面
  }
}

接口NetUtil和GUI用到的类的实现这里从略。到此,服务器端程序已实现。下面是客户端程序的实现。

客户端程序的实现:客户端程序首先实现的是类Reciever。代码如下(程序清单2。1。1):

程序清单2。1。1

class Receiver extends Thread {
PrintStream os;
BufferedReader is;
Socket clientSocket = null;
public Receiver (Socket socket) {
clientSocket = socket; //Socket实例是在applet初始化时创建的,而不能在线程内创建
}
public void run () {
if (clientSocket == null) {
return;
}
try {
os = new PrintStream(clientSocket.getOutputStream());
is = new BufferedReader(new InputStreamReader(clientSocket.getInputStream()));
} catch (Exception se) {
String err = se.getMessage() + "\n";
}
String strTalking;
String strStart = "";
strStart = NetUtil.CHATSTART + "\n";
strStart += strUserID;
os.println(strStart);
os.flush();
while (true) {
try {
while ((strTalking = new String(is.readLine())) != null) {
if (strTalking.equals(NetUtil.PERSONINFO)) { //显示在线客户信息
while ((strTalking = new String(is.readLine()))!= null) {
if (strTalking.equals(NetUtil.DIVIDEDLINE)) {
break;
}
if (!strTalking.equals("\n") && !strTalking.equals("")) {
addUser(strTalking);
}}}
else if (strTalking.equals(NetUtil.ISCALLED)) {
String strCallName = (String)is.readLine();
strCallingName = strCallName;
textNotify.setEnabled(true);
textNotify.setText(strCallName + " is calling you!");
}
else if (strTalking.equals(NetUtil.RESPONSE)) {
String strResponseName = (String)is.readLine();
textNotify.setText("You are chatting with " + strResponseName);
strCallingName = strResponseName;
isTalkWith = true;
choiceUser.addItem(strResponseName);
}
else if (strTalking.equals(NetUtil.PERSONALRECEIVE)) {
String strWhoIs = (String)is.readLine();
String strContent = (String)is.readLine();
textTalkContent.append(strWhoIs + " speak to you: "+ strContent + "\n");
}
else {
strTalking += "\n";
textTalkContent.append(strTalking);
}}
} catch (Exception io_e) {
System.out.println(io_e.getMessage());
}}}}

该类用来实现与服务器端数据同步。当有客户发言或者有客户和另一客户私下交谈时,该类将立即更新这些数据,在客户端显示出来。所以,该类在客户会话期间一直在后台运行。负责发送客户请求的类是RequestSender。RequestSender用成员方法chatAll ()和chatOne ()分别实现大厅发言和私下交谈,其实现可以参照以上程序。客户端applet的实现代码这里从略。至此,客户端程序也已完成。以下是客户端程序运行时的快照(图2。4。1和图2。4。2)。

图2。4。1
图2。4。1

图2。4。2
图2。4。2

5.系统扩展及补充说明
以上实例由于篇幅原因只实现很少一部分功能,但能体现该即时通信系统的总体设计思想,而且很容易实现功能扩展。例如,可以实现公共数据处理类(CmDataProcessor)与DBMS的交互,实现数据的持久化(persistence);可以对applet实现数字签名来加大它对客户机的访问权限,从而实现文件的传输,实现多媒体交互;可以实现applet与servlet的对话,将比较复杂的业务逻辑交给应用服务器中的EJB来处理等。由于本人水平有限,难免有错误之处,欢迎批评指正。

关于作者
杨健,中南工业大学计算机科学与技术专业硕士研究生,参与过大型MIS系统开发,网站建设等,现在正致力于Java技术的研究。Email: ytjcopy@263.net

对于这个系列里的问题,每个学Java的人都应该搞懂。当然,如果只是学Java玩玩就无所谓了。如果你认为自己已经超越初学者了,却不很懂这些问题,请将你自己重归初学者行列。内容均来自于CSDN的经典老贴。

问题一:我声明了什么!

String s = "Hello world!";

许多人都做过这样的事情,但是,我们到底声明了什么?回答通常是:一个String,内容是“Hello world!”。这样模糊的回答通常是概念不清的根源。如果要准确的回答,一半的人大概会回答错误。
这个语句声明的是一个指向对象的引用,名为“s”,可以指向类型为String的任何对象,目前指向"Hello world!"这个String类型的对象。这就是真正发生的事情。我们并没有声明一个String对象,我们只是声明了一个只能指向String对象的引用变量。所以,如果在刚才那句语句后面,如果再运行一句:

String string = s;

我们是声明了另外一个只能指向String对象的引用,名为string,并没有第二个对象产生,string还是指向原来那个对象,也就是,和s指向同一个对象。

问题二:"=="和equals方法究竟有什么区别?

==操作符专门用来比较变量的值是否相等。比较好理解的一点是:
int a=10;
int b=10;
则a==b将是true。
但不好理解的地方是:
String a=new String("foo");
String b=new String("foo");
则a==b将返回false。

根据前一帖说过,对象变量其实是一个引用,它们的值是指向对象所在的内存地址,而不是对象本身。a和b都使用了new操作符,意味着将在内存中产生两个内容为"foo"的字符串,既然是“两个”,它们自然位于不同的内存地址。a和b的值其实是两个不同的内存地址的值,所以使用"=="操作符,结果会是false。诚然,a和b所指的对象,它们的内容都是"foo",应该是“相等”,但是==操作符并不涉及到对象内容的比较。
对象内容的比较,正是equals方法做的事。

看一下Object对象的equals方法是如何实现的:
boolean equals(Object o){

return this==o;

}
Object对象默认使用了==操作符。所以如果你自创的类没有覆盖equals方法,那你的类使用equals和使用==会得到同样的结果。同样也可以看出,Object的equals方法没有达到equals方法应该达到的目标:比较两个对象内容是否相等。因为答案应该由类的创建者决定,所以Object把这个任务留给了类的创建者。

看一下一个极端的类:
Class Monster{
private String content;

boolean equals(Object another){ return true;}

}
我覆盖了equals方法。这个实现会导致无论Monster实例内容如何,它们之间的比较永远返回true。

所以当你是用equals方法判断对象的内容是否相等,请不要想当然。因为可能你认为相等,而这个类的作者不这样认为,而类的equals方法的实现是由他掌握的。如果你需要使用equals方法,或者使用任何基于散列码的集合(HashSet,HashMap,HashTable),请察看一下java doc以确认这个类的equals逻辑是如何实现的。

问题三:String到底变了没有?

没有。因为String被设计成不可变(immutable)类,所以它的所有对象都是不可变对象。请看下列代码:

String s = "Hello";
s = s + " world!";

s所指向的对象是否改变了呢?从本系列第一篇的结论很容易导出这个结论。我们来看看发生了什么事情。在这段代码中,s原先指向一个String对象,内容是"Hello",然后我们对s进行了+操作,那么s所指向的那个对象是否发生了改变呢?答案是没有。这时,s不指向原来那个对象了,而指向了另一个String对象,内容为"Hello world!",原来那个对象还存在于内存之中,只是s这个引用变量不再指向它了。
通过上面的说明,我们很容易导出另一个结论,如果经常对字符串进行各种各样的修改,或者说,不可预见的修改,那么使用String来代表字符串的话会引起很大的内存开销。因为String对象建立之后不能再改变,所以对于每一个不同的字符串,都需要一个String对象来表示。这时,应该考虑使用StringBuffer类,它允许修改,而不是每个不同的字符串都要生成一个新的对象。并且,这两种类的对象转换十分容易。
同时,我们还可以知道,如果要使用内容相同的字符串,不必每次都new一个String。例如我们要在构造器中对一个名叫s的String引用变量进行初始化,把它设置为初始值,应当这样做:
public class Demo {
  private String s;
  …
  public Demo {
    s = "Initial Value";
  }
  …
}
而非
s = new String("Initial Value");
后者每次都会调用构造器,生成新对象,性能低下且内存开销大,并且没有意义,因为String对象不可改变,所以对于内容相同的字符串,只要一个String对象来表示就可以了。也就说,多次调用上面的构造器创建多个对象,他们的String类型属性s都指向同一个对象。
上面的结论还基于这样一个事实:对于字符串常量,如果内容相同,Java认为它们代表同一个String对象。而用关键字new调用构造器,总是会创建一个新的对象,无论内容是否相同。
至于为什么要把String类设计成不可变类,是它的用途决定的。其实不只String,很多Java标准类库中的类都是不可变的。在开发一个系统的时候,我们有时候也需要设计不可变类,来传递一组相关的值,这也是面向对象思想的体现。不可变类有一些优点,比如因为它的对象是只读的,所以多线程并发访问也不会有任何问题。当然也有一些缺点,比如每个不同的状态都要一个对象来代表,可能会造成性能上的问题。所以Java标准类库还提供了一个可变版本,即StringBuffer。

问题四:final关键字到底修饰了什么?

final使得被修饰的变量"不变",但是由于对象型变量的本质是“引用”,使得“不变”也有了两种含义:引用本身的不变,和引用指向的对象不变。

引用本身的不变:
final StringBuffer a=new StringBuffer("immutable");
final StringBuffer b=new StringBuffer("not immutable");
a=b;//编译期错误

引用指向的对象不变:
final StringBuffer a=new StringBuffer("immutable");
a.append(" broken!"); //编译通过

可见,final只对引用的“值”(也即它所指向的那个对象的内存地址)有效,它迫使引用只能指向初始指向的那个对象,改变它的指向会导致编译期错误。至于它所指向的对象的变化,final是不负责的。这很类似==操作符:==操作符只负责引用的“值”相等,至于这个地址所指向的对象内容是否相等,==操作符是不管的。

理解final问题有很重要的含义。许多程序漏洞都基于此—-final只能保证引用永远指向固定对象,不能保证那个对象的状态不变。在多线程的操作中,一个对象会被多个线程共享或修改,一个线程对对象无意识的修改可能会导致另一个使用此对象的线程崩溃。一个错误的解决方法就是在此对象新建的时候把它声明为final,意图使得它“永远不变”。其实那是徒劳的。

问题五:到底要怎么样初始化!

本问题讨论变量的初始化,所以先来看一下Java中有哪些种类的变量。
1. 类的属性,或者叫值域
2. 方法里的局部变量
3. 方法的参数

对于第一种变量,Java虚拟机会自动进行初始化。如果给出了初始值,则初始化为该初始值。如果没有给出,则把它初始化为该类型变量的默认初始值。

int类型变量默认初始值为0
float类型变量默认初始值为0.0f
double类型变量默认初始值为0.0
boolean类型变量默认初始值为false
char类型变量默认初始值为0(ASCII码)
long类型变量默认初始值为0
所有对象引用类型变量默认初始值为null,即不指向任何对象。注意数组本身也是对象,所以没有初始化的数组引用在自动初始化后其值也是null。

对于两种不同的类属性,static属性与instance属性,初始化的时机是不同的。instance属性在创建实例的时候初始化,static属性在类加载,也就是第一次用到这个类的时候初始化,对于后来的实例的创建,不再次进行初始化。这个问题会在以后的系列中进行详细讨论。

对于第二种变量,必须明确地进行初始化。如果再没有初始化之前就试图使用它,编译器会抗议。如果初始化的语句在try块中或if块中,也必须要让它在第一次使用前一定能够得到赋值。也就是说,把初始化语句放在只有if块的条件判断语句中编译器也会抗议,因为执行的时候可能不符合if后面的判断条件,如此一来初始化语句就不会被执行了,这就违反了局部变量使用前必须初始化的规定。但如果在else块中也有初始化语句,就可以通过编译,因为无论如何,总有至少一条初始化语句会被执行,不会发生使用前未被初始化的事情。对于try-catch也是一样,如果只有在try块里才有初始化语句,编译部通过。如果在catch或finally里也有,则可以通过编译。总之,要保证局部变量在使用之前一定被初始化了。所以,一个好的做法是在声明他们的时候就初始化他们,如果不知道要出事化成什么值好,就用上面的默认值吧!

其实第三种变量和第二种本质上是一样的,都是方法中的局部变量。只不过作为参数,肯定是被初始化过的,传入的值就是初始值,所以不需要初始化。

问题六:instanceof是什么东东?

instanceof是Java的一个二元操作符,和==,>,<是同一类东东。由于它是由字母组成的,所以也是Java的保留关键字。它的作用是测试它左边的对象是否是它右边的类的实例,返回boolean类型的数据。举个例子:

String s = "I AM an Object!";
boolean isObject = s instanceof Object;

我们声明了一个String对象引用,指向一个String对象,然后用instancof来测试它所指向的对象是否是Object类的一个实例,显然,这是真的,所以返回true,也就是isObject的值为True。
instanceof有一些用处。比如我们写了一个处理账单的系统,其中有这样三个类:

public class Bill {//省略细节}
public class PhoneBill extends Bill {//省略细节}
public class GasBill extends Bill {//省略细节}

在处理程序里有一个方法,接受一个Bill类型的对象,计算金额。假设两种账单计算方法不同,而传入的Bill对象可能是两种中的任何一种,所以要用instanceof来判断:

public double calculate(Bill bill) {
  if (bill instanceof PhoneBill) {
    //计算电话账单
  }
  if (bill instanceof GasBill) {
    //计算燃气账单
  }
  …
}
这样就可以用一个方法处理两种子类。

然而,这种做法通常被认为是没有好好利用面向对象中的多态性。其实上面的功能要求用方法重载完全可以实现,这是面向对象变成应有的做法,避免回到结构化编程模式。只要提供两个名字和返回值都相同,接受参数类型不同的方法就可以了:

public double calculate(PhoneBill bill) {
  //计算电话账单
}

public double calculate(GasBill bill) {
  //计算燃气账单
}

所以,使用instanceof在绝大多数情况下并不是推荐的做法,应当好好利用多态。