一、引言
WEB聊天室(Chatroom)是ICP提供的常用服务之一,它给网络用户提供了在线实时交流的机会。使用WEB聊天室不需要安装象IRC、mIRC、MSChat等专门的软件,而只需要浏览器即可。也就是说,聊天过程实际上是通过HTTP协议进行的。WEB聊天室的实现方案有多种,目前常见的包括:基于CGI、基于Java、基于ActiveX和基于ASP的实现方案等。各种方案都有其独特之处,需要根据场合选用。下面我们就来讨论这四种方案的具体实现过程并对它们进行比较。
二、基于CGI的聊天室
CGI(Common Gateway Interface,通用网关接口)往往被认为是一种效率较低的WEB动态页面实现方式。这是因为对于客户端(即浏览器端)的每个CGI请求,服务器端都会产生一个新的进程。其实对于UNIX平台的服务器来说,采用CGI方式并不会有效率问题,因为UNIX是基于进程的操作系统,当参加聊天的用户数很多而导致产生大量的CGI进程时,UNIX有足够能力来进行进程的管理调度,这时执行性能主要取决于机器的硬件配置。而对于NT平台的服务器来说,就不一样了。因为NT是针对线程优化的操作系统,即它管理的最小单位是线程而不是进程,大量的CGI进程会增加很多无谓的开销,从而影响系统的执行效率。
编写CGI程序可以采用多种语言和工具,如C/C++,Perl,PHP3等,在Windows平台上还可以用VB,Delphi等编写WinCGI程序(此时CGI程序与Web服务器之间不是通过stdin/stdout进行数据交互,而是通过临时文件)。为了统一调试的方便,本文中所有聊天室程序都以NT Server的IIS 4.0服务器作为平台。下面我们以Perl语言来编写一个简单的基于CGI方式的WEB聊天室程序,如果这个程序要在Unix的Apache上运行,需要作少量的改动。
一般来说,基于CGI方式的聊天室程序的设计思想是这样的:将每个聊天用户的发言(Message)按后进先出的顺序存入到数据库或文件中,并在用户端进行定时刷新来获取最新的Message数据。通过在HTML文件的<HEAD>部分插入相应的<META>元素,即可实现定时刷新,例如:
<META HTTP-EQUIV="Refresh" CONTENT="4">:表示在4秒后重新调入本页面
具体实现过程如下:
(1)首先要为IIS4安装Perl解释器。笔者选用的是ActivePerl(版本号:Build 517),安装时让其自动对IIS4进行配置,使得IIS4能够调用Perl.exe去解释以.pl为后缀的CGI程序。如果Web服务器是IIS3或PWS,需要修改注册表来达到此目的(详见ActivePerl的联机文档)。
(2)在IIS4的管理界面MMC里建立虚拟目录,例如Chat,对应物理目录C:\Chat,将此虚拟目录属性设定为“读取”和“执行”。
(3)在C:\Chat下建立主框架HTML文件Default.htm,将画面分为上下两个frame,上面的显示所有用户发言,下面的用于当前用户输入发言。同时建立message.htm作为存储用户发言的模板,login.htm用于用户输入名字进入聊天室。
default.htm
<FRAMESET ROWS="*,45">
<FRAME SRC=message.htm>
<FRAME SRC=login.htm>
</FRAMESET>
message.htm
<html><head><meta http-equiv="Refresh" content="4"></head><body>
<br>欢迎光临CGI聊天室!
</body></html>
login.htm
<html><body>
<form action="chat.pl" method="post">输入你的名字:
<input type=text name=username>
<input type=submit value="进入聊天室">
<input type=hidden name=message value="我来到了聊天室!">
</form></body></html>
(4)建立主程序chat.pl文件:
chat.pl
print "Content-type: text/html\n\n";
&get_form_data; #获得用户输入
open(MESSAGE,"c:/chat/message.htm"); #获取message.htm的内容
@lines=<MESSAGE>;
close(MESSAGE);
$now_string = localtime;
@thetime = split(/ +/,$now_string); #获取当前时间,存放在$thetime[3]中
print "<html><body>\n";
#显示发言输入Form
print "<form action=chat.pl method=\"post\">\n";
print "<input name=username type=hidden value=\"$formdata{'username'}\">\n";
print "<input type=text name=message size=40>\n";
print "<input type=submit value=\"发言\"></form>\n";
if($formdata{'message'} ne "")
{
#新的发言存放于$newmessage中
$newmessage = "<br>$formdata{'username'}:$formdata{'message'} (时间:$thetime[3])\n";
#重写message.htm
open (NEW, ">c:/chat/message.htm");
print NEW "<html><head><meta http-equiv=\"Refresh\" content=\"4\"></head><body>\n";
print NEW $newmessage;
#最近的发言最多保存10行
$limit_lines=10;
if( $#lines < 10 )
{ $limit_lines=$#lines; }
for ($i = 1; $i < $limit_lines; $i++)
{ print NEW "$lines[$i]"; }
print NEW '</body></html>';
close(NEW);
}
print "</body></html>\n";
exit 0;
#获取Form数据的子过程
sub get_form_data {
$buffer = "";
read(STDIN, $buffer, $ENV{'CONTENT_LENGTH'});
@pairs=split(/&/,$buffer);
foreach $pair (@pairs)
{
@a = split(/=/,$pair);
$name=$a[0];
$value=$a[1];
$value =~ s/%([a-fA-F0-9][a-fA-F0-9])/pack("C", hex($1))/eg;
$value =~ s/~!/ ~!/g;
$value =~ s/\+/ /g;
#下面两句过滤掉发言中的HTML Tag
$value =~ s/\</\<\;/g;
$value =~ s/\>/\>\;/g;
$value =~ s/\r//g;
push (@data,$name);
push (@data, $value);
}
%formdata=@data;
%formdata;
}
(4)在浏览器的URL地址栏中输入 http://localhost/chat/ 即可进入聊天室。
运行时的画面如图1所示:
在图中最新的发言最先显示,读者可改变程序以支持更多功能。在实际使用中为了避免多用户并发写message.htm,最好用数据库存储发言数据。可见,基于CGI方式的聊天室并非真正实时的,新的发言需要等到下一次自动刷新时才能看到。要做到真正的实时聊天,必需建立客户和服务器端的永久性网络连接,下面所述的两种方案:使用Java Applet和ActiveX就是这种思想。
三、基于Java的聊天室
基于Java的聊天室其过程是这样的:在返回给客户端的HTML页面里插入Java Applet,该Applet与Web服务器上运行的专用聊天服务器程序进行连接,当客户端用户输入新发言时,发言被传送到聊天服务器并由其向其他的聊天用户进行广播。Java聊天室的最大优点是在网络带宽比较理想的前提下(如局域网内),能真正作到实时性。
众所周知,Java是一种跨平台的语言,也就是说,用Java编写的程序具有很高的可移植性,可事实上并非如此,Java的中文兼容性问题一直是众多Java程序员所头疼的事情,这主要表现在中文文本的读写、中文的输入输出、中文的网络传输等方面,汉字的GB内码往往被当作Unicode码进行处理而导致乱码。
如果聊天室程序要支持中文,就要涉及汉字的显示、传输与输入。所幸的是,随着Java编译器版本的提高,Java的国际化和本地化问题也逐步得到解决。在JDK 1.1以上版本中,通过使用BufferedReader和BufferedWriter等Java类进行流输入输出,可以解决Java Applet的汉字传输和显示问题。但经过试验,经过JDK1.1.6编译完的Applet,在NT服务器所装的IE4中文版浏览器下(版本4.72.3110.8;SP1),仍然出现一些汉字不能正常显示而用?代替的情况,如”问题”经过传输后变成”问?”。针对这种情况,我们在聊天室程序中仍然使用DataInputStream和DataOutputStream进行网络流传输,而利用它们的readUTF()和writeUTF()函数对传输内容进行统一的UTF编码,即:
//…传送端
String s=”中文问题”
Output.writeUTF(s); // Unicode码到UTF码
//…接收端
String s=Input.readUTF(); // UTF码到Unicode码
使用这种方法实现了中英文的完全正确传输,而且另一个好处是聊天室程序也能被Visual J++ 1.1或其他只兼容到JDK1.0的编译器所编译。汉字传输问题是解决了,但在大部分版本(3,4,5)的IE浏览器中,Java Applet的TextField仍然不能输入汉字,而Netscape浏览器和JDK所带的AppletViewer却可以。这可能是因为IE使用的是Microsoft自己的JVM(Java Virtual Machine)的缘故。可见,Java的跨平台特性仍然依赖于编译器和解释器的兼容性。
虽然在IE中Applet的TextField顽固不化的不接受中文,我们可以使用一个小技巧来解决这个问题,那就是不使用Java的TextField类,而是依靠HTML的Form的Text元素进行输入,然后把输入结果传递给Applet。在HTML页面里加入JavaScript程序,与该页面的Applet进行通讯,即
<script language="javascript">
function SendIt()
{
document.Applet1.Procedure(); //调用Applet1的Procedure过程
}
</script>
<applet code=”appletcode.class” name=”Applet1”>…
Java聊天室的具体实现过程如下:
(1)为测试兼容性,我们安装了两种Java编译器,Visual J++ 1.1和JDK。笔者安装的JDK版本是1.1.6 for Win,安装到D:\JDK目录,装完后打开NT的控制面板-“系统”-“环境”,添加D:\JDK\BIN到PATH环境变量,并添加一个CLASSPATH环境变量,值为“.;D:\JDK\LIB\CLASSES.ZIP”。
(2)因为Java的字符都是Unicode码,为统一网络传输起见,聊天服务器程序我们亦采用Java编写。它是一个Java Application,其源程序如下:
Server.java
import java.io.*;
import java.net.*;
import java.util.*;
public class Server extends Thread
{
protected ServerSocket listen_socket;
public Vector clients=new Vector(); //clients用于存储各个连接的用户
// 出错时打印信息并退出
public static void fail(Exception e,String msg)
{
System.err.println(msg+":"+e);
System.exit(1);
}
//创建ServerSocket并监听客户端连接
public Server()
{
try
{
// 监听端口为6543
listen_socket=new ServerSocket(6543);
}
catch(IOException e){ fail(e,"创建ServerSocket失败!");}
System.out.println("聊天服务器启动: 监听端口号为6543... ");
this.start();
}
public void run()
{
try
{
while(true)
{
Socket client_socket=listen_socket.accept();
//创建与客户端连接线程
Connection c=new Connection(client_socket,this);
clients.addElement(c); //存储
System.out.println("新用户连接:
"+client_socket.getInetAddress());
}
}
catch(IOException e) {fail(e,"监听时出现错误!");}
}
//主函数
public static void main (String[] args) throws IOException
{
new Server(); //创建Server主线程
}
}
// 处理同客户端连接的线程
class Connection extends Thread
{
public Socket client;
protected DataInputStream in; // 从客户端读的缓冲
protected DataOutputStream out; // 向服务器端输出的缓冲
protected Server serv;
// 初始化缓冲流并启动线程
public Connection(Socket client_socket,Server sv)
{
client=client_socket;
serv=sv; //保存Server线程
try
{
in=new DataInputStream(client.getInputStream());
}
catch(IOException e)
{
try{ client.close();}
catch(IOException e2)
{
System.err.println("获取Socket流时发生错误:"+e2);
return;
}
}
this.start();
}
public void run()
{
String line;
StringBuffer revline;
int len,nums,i;
try{
for(;;){
line=in.readUTF(); //读入一行 UTFCode String—>UniCode String
System.out.println(line);
if(line==null) break;
//进行广播
nums=serv.clients.size();
for(i=0;i<nums;i++)
{
//取得每个Connection
Connection c=(Connection)serv.clients.elementAt(i);
out=new DataOutputStream(c.client.getOutputStream());
out.writeUTF(line); //广播,用UTF编码进行传输
}}
}
catch(IOException e)
{
System.err.println("与客户端通信时发生错误:"+e);
try{client.close();}
catch(IOException e2){ System.err.println("不能关闭客户端连接:"+e2);}
}
}
}
(3)编写客户端Applet,其源代码如下:
AppletClient.java
import java.applet.*;
import java.awt.*;
import java.io.*;
import java.net.*;
import java.util.*;
public class AppletClient extends Applet
{
Socket s;
DataInputStream in; // 读缓冲
DataOutputStream out; // 写缓冲
TextArea outputarea; // 显示发言的区域
StreamListener listener; //监听线程用于显示服务器发回的消息
String username; //聊天用户名
public void init()
{
try{
username=getParameter("username"); //获取用户名参数
//由于Applet的安全限制,只能同下载服务器进行连接
s=new Socket(this.getCodeBase().getHost(),6543);
in=new DataInputStream(s.getInputStream());
out=new DataOutputStream(s.getOutputStream());
outputarea=new TextArea();
outputarea.setEditable(false); //仅用于显示
this.setLayout(new BorderLayout());
this.add("Center",outputarea);
listener=new StreamListener(in,outputarea);
outputarea.appendText("连接到服务器
"+s.getInetAddress().getHostName()
+":"+s.getPort()+"\n");
out.writeUTF(username+" 进入聊天室.");
}
catch(IOException e){ this.showStatus(e.toString()); }
}
public void SendToServer(String s)
//此过程供网页内的JavaScript程序调用,将s发送到服务器
{
try{
Date nowTime=new Date();
out.writeUTF(username+":"+s+"("+nowTime.toLocaleString()+")");
}
catch(IOException e){ this.showStatus(e.toString()); }
}
public void Clear()
//此过程供网页内的JavaScript程序调用,清除显示区
{
outputarea.setText("");
}
}
// 客户端监听线程
class StreamListener extends Thread
{
TextArea output;
DataInputStream in;
public StreamListener(DataInputStream in,TextArea output)
{
this.in=in;
this.output=output;
this.start();
}
public void run()
{
String line;
try
{
for(;;){
line=in.readUTF();
if(line==null) break;
//将服务器发回信息添加到textarea显示区
output.appendText(line+"\n");
}
}
catch(IOException e){ output.appendText(e.toString()+"\n");}
finally{ output.appendText("连接被服务器关闭.\n");}
}
}
(4)由于该Applet使用username参数来确定聊天用户名,故在此我们使用ASP动态传递此参数。登录及聊天的HTML代码均由default.asp控制。
Default.asp
<HTML>
<HEAD>
<TITLE>聊天室</TITLE>
<script language="javascript">
function SendIt() //发送发言到Applet
{
document.AppletClient.SendToServer(document.CHATFORM.textField.value);
document.CHATFORM.textField.value="";
}
function ClearDisplay()
{
document.AppletClient.Clear();
}
</script>
</HEAD>
<BODY>
<%
If trim(request("username"))="" then
'显示用户登录Form
%>
<form name="LOGINFORM" action="default.asp">
输入你的代号:<input type="text" name="username" size=20>
<input type="submit" value="进入聊天室">
</form>
<%
else '否则显示聊天Form
%>
<APPLET CODE="AppletClient.class" name=AppletClient WIDTH=500 HEIGHT=300>
<PARAM name="username" value="<%=request("username")%>">
</APPLET>
<form name="CHATFORM" onSubmit="SendIt();return false;">
<input type="text" name="textField" size=40">
<input type="button" value="发送" onClick="SendIt()">
<input type="button" value="清除" onClick="ClearDisplay()">
</form>
<% end if%>
</BODY>
</HTML>
(5) 编译Server.java和AppletClient.java,如果使用VJ++,只需打开.java文件进行Build即可。如果用JDK,使用JAVAC 进行编译。最后生成文件Server.class和AppletClient.class。将这两个.class文件连同default.asp文件放入C:\JAVACHAT,并在MMC中为C:\JAVACHAT建立虚拟目录JAVACHAT。
(6)启动Server,对于VJ++,命令是: jview Server
对于JDK,命令是 java Server.class
(7)在浏览器的URL地址栏中输入 http://localhost/javachat/ 即可进入聊天室。
运行结果如下图所示:
|