摘 要:本文讨论了用Java开发Web聊天室系统的优点,并给出了一个Java聊天室系统的实例。作者解决了其中的中文传输、显示及输入的问题。该聊天室系统具有高效性、健壮性和灵活性,达到了预期的设计目标。
关键字: WEB聊天室 JAVA UTF-8 异常 哈希表 广播
一、概述
聊天室(Chatroom)是Web站点提供的常用服务之一,它给网络用户带来了在线实时交流的机会,而且使用起来不需要安装专门的聊天软件,只需要浏览器即可。Web聊天室系统由于其方便、灵活和易于使用的特点而广受欢迎。目前开发Web聊天室系统的方法主要有以下几种:CGI,Java,ActiveX,ASP等等。相比较起来,Java语言具有其优越之处[1]:一是跨平台和可移植性好。Java Application和Java Applet程序几乎能在所有平台上编译、执行,而象含ActiveX的页面主要针对x86的win32系统,且在Netscape浏览器中运行时需额外安装plug-in。二是使用Java语言编写的聊天室能够做到真正的实时聊天。常见的CGI、ASP等方法一般是通过无连接的HTTP协议来传输数据,需要靠HTML页面的自动定时刷新来模拟聊天过程。而Java Applet能够与服务器端建立永久的TCP/IP连接,用户的发言能够被马上传输和广播,而且也不需要传输额外的HTML内容。三是Java语言本身的功能非常适合于编写网络应用程序,如鲁棒性(Robust),完善的Net类库和多线程支持等等。本文基于Java语言来开发一个完整的Web聊天室系统。
二、结构与目标
基于Java的Web聊天室系统包括聊天服务器和客户端两部分。聊天服务器是一个Java Application,与Web服务器程序运行在同一机器上。客户端部分即是一个含Java Applet的HTML页面,它由Web服务器传送给客户端浏览器,交由浏览器的Java虚拟机(VM)解释执行。该Applet初始化后与聊天服务器进行连接,聊天服务器对于每个连接请求产生一个连接线程(Connection Thread),来维护和管理与该客户端的会话。客户端的发言被传送到服务器端后由其向其他客户进行广播(Broadcast),达到相互聊天的目的。Java聊天室系统结构如图1所示:
图1 Java聊天室的结构
为了让这个聊天室系统能够真正实用,必须达到以下要求:
⑴完善的支持中文。由于Java编译器版本及运行环境的差异等原因,在Java语言的中文处理中常出现乱码等现象,表现在中文显示与网络传输不正常、中文不能输入等方面。在这个聊天室系统中我们将彻底解决中文的兼容性问题。
⑵具有健壮性。即聊天室系统能够处理各种异常,能够识别和控制客户端的各种行为,能够返回清理不正常退出后所分配的系统资源,能够踢出超时连接用户以减轻服务器负载等。虽然Java语言本身能够自动收集处理无用的对象[2],但我们仍然需要作一定的清理工作。
⑶广泛的适应性。因为我们不能要求聊天用户必须使用某种浏览器或操作系统,因此所编写的Java程序,尤其是Applet,必须能在各种平台的各个版本的浏览器上都能正常运行。考虑到网络用户的使用情况,我们定的标准是能适应以下版本的浏览器:Netscape 3.x,Netscape Communicator 4.x,Internet Explorer 3.x、4.x、5.x中英文版。
三、聊天服务器
在聊天服务器中,我们使用哈希表(Hashtable)来存储所有的连接线程。主线程为ChatServer,对于每个新的客户连接请求产生一个Connection线程。同时我们还运行了一个检查线程CheckActiveTimer,它相当于一个定时器,每隔一定时间就扫描所有的客户连接线程(即扫描Hashtable),检查每个客户连接是否超时(例如很长时间没有发言或者死机),并给出警告或直接踢出(Kick)用户。
在网络传输过程中,我们使用字节输入输出流DataInputStream和DataOutputStream的writeUTF、readUTF方法进行传送接收,这两个方法以UTF-8编码方式来对Unicode字符串进行编码和解码,这样我们就能正确的进行中英文的传送。虽然在JDK 1.1以上版本编译器中,我们可以使用基于字符的流,如BufferdReader、PrintWriter等类进行网络传输,但是经过试验,在某些旧的Java VM上(如IE4所带的VM)仍然出现一些汉字传输后变成?字符的现象,所以我们不采用这种方式。
聊天服务器的主要程序段代码及其解释如下:
⑴主线程ChatServer的定义及初始化
public class ChatServer extends Thread { //主线程ChatServer
public Hashtable chatusers; //存储与所有聊天用户的连接线程
protected ServerSocket listen_socket; //监听Socket
protected CheckActiveTimer check; //定时检查用户活动情况的线程
public final static int PORT=6543; //默认端口号
public final static long WARNTIMEOUT=180000l; //超过3分钟用户无反应则警告
public final static long KICKTIMEOUT=240000l; //超过4分钟用户无反应则被踢出
//类初始化过程
public ChatServer() {
chatusers=new Hashtable(); //分配哈希表
check=new CheckActiveTimer(this); //启动检查线程
try { //创建监听Socket
listen_socket=new ServerSocket(PORT); }
catch(IOException e) { fail("创建监听Socket失败"); }
this.start(); //启动线程的执行
}
⑵主线程的执行过程run()
public void run(){
try{ while(true){
Socket client_socket=listen_socket.accept();
//创建与客户端连接线程
Connection c=new Connection(client_socket,this);
}}
catch(IOException e)
{ error("与新登录用户连接失败"); }
}
⑶检查连接用户活动情况的方法checkit()
实际上,ChatServer的checkit()方法是在CheckActiveTimer线程里被定时调用的。
public void checkit() {
Vector kill=new Vector(); //用Vector存储要被踢出的超时用户
long now=(new Date()).getTime(); //取现在的时间,单位:毫秒
Enumeration e=chatusers.keys();
// 遍历连接线程,检查每个用户的上次发言时间
while(e.hasMoreElements()){
String key=(String)e.nextElement();
Connection c=(Connection)chatusers.get(key);
long inactive=now-c.lasttime;
if(inactive>KICKTIMEOUT) { //连接超时大于踢出时间
kill.addElement(c);
}else if(inactive>WARNTIMEOUT) //连接超时大于警告时间
{
try{ //向该用户发出警告消息
c.out.writeUTF("注意:您将在"+((KICKTIMEOUT-inactive)/1000)+"秒后被踢出聊天室");
c.out.flush();
}
catch(IOException e1){ error("警告用户"+c.logname+"失败"); }}}
// 检查完毕后,踢出超时用户
for(int i=0;i<kill.size();i++){
Connection c=(Connection)kill.elementAt(i);
log("准备踢出用户"+c.logname);
try{ c.client_socket.close(); }
catch(IOException e2) {
error("无法踢出用户:"+c.logname); }}}
⑷检查用户是否超时活动的线程CheckActive
class CheckActiveTimer extends Thread{
…
public void run() {
while(true) {
if(server!=null) server.checkit();
try{ sleep(PERIOD); } //线程暂时中止PERIOD时间
catch(InterruptedException e){ System.err.println("错误:检查线程被中止"); }
}}}
⑸处理同客户端连接的线程Connection
class Connection extends Thread{
public Socket client_socket;
public long lasttime; //上次活动时间
protected DataInputStream in; // 读缓冲流
protected DataOutputStream out; // 写缓冲流
public String hashkey; // 标志此Connection的字符串
public String username; // 聊天用户代号
public String logname;
protected Hashtable chatusers; // 引用ChatServer的chatusers
public Connection(Socket client_socket,ChatServer server) { // 初始化
this.client_socket=client_socket;
chatusers=server.chatusers;
lasttime=(new Date()).getTime();
username="未知";
try{
in=new DataInputStream(client_socket.getInputStream());
out=new DataOutputStream(client_socket.getOutputStream());
}catch(IOException e) {
System.err.println("错误:获取客户端Socket流时发生错误");
try{ client_socket.close();}
catch(IOException e2) {
System.err.println("错误:无法关闭客户端Socket");
}
this.stop(); //停止线程
return;
}
hashkey=client_socket.getInetAddress()+":"+client_socket.getPort();
logname="["+hashkey+"/"+username+"]";
chatusers.put(hashkey,this); // 加入用户列表
this.start();
}
public void run() {
String line;
boolean command;
try{ for(;;){
command=false;
line=in.readUTF(); //读入一行
if(line==null) break;
lasttime=(new Date()).getTime();
if(line.startsWith("$username")) { //用户登录名字
username=line.substring(9);
logname="["+hashkey+"/"+username+"]";
broadcast(username+"进入聊天室,目前聊天室用户人数为"+chatusers.size());
command=true;
}
if(line.startsWith("list ")) { // 列用户命令
Enumeration e=chatusers.keys();
String msg="目前在聊天室的用户有:\n";
while(e.hasMoreElements()) {
String key=(String)e.nextElement();
Connection c=(Connection)chatusers.get(key);
msg=msg+c.username+"\n";
}
out.writeUTF(msg); out.flush();
command=true;
}
if(!command) broadcast(username+":"+line); //进行广播
}
}
catch(IOException e){
System.err.println("错误:与"+logname+"通信时发生错误");
}
finally { // 与客户端通信失败后,资源清理工作
try{
client_socket.close(); //关闭Socket
System.out.println("关闭与"+logname+"的连接");
}
catch(IOException e){
System.err.println("错误:不能关闭与"+logname+"的连接");
}
finally{ //最终删除线程
client_socket=null;
chatusers.remove(hashkey); //从哈希表中删除
try{
System.out.println(logname+"离开了聊天室.");
broadcast(username+"离开了聊天室.");
}catch(IOException e){
System.err.println("错误:广播错误");
}
this.stop();
}
} }
|