创建基于 Ajax 的 IM 客户端

使用 Jabber 和 Web 页面将 IM 通信量转变为 Web 通信量

能够与同事和朋友进行即时消息(IM)通信是一种极大的便利,但出于安全性方面的考虑,有些环境却禁止在工作区使用即时消息客户端。本文中的练习解决了安全性方面的种种担心,向您展示了如何使用 Ajax 创建基于 Web 的 IM 客户端,这种客户端通过创建即时消息 bot 和对应的 Web 应用程序将 IM 通信量转变为纯 Web 通信量。虽然它不是一种生产应用程序,却展示了几种极好的 Ajax 技术,比如如何使用 Prototype 进行更简便的 DOM 处理以及如何轻松地一次或多次更新 Web 页面的某些部分。

Nicholas Chase, 自由撰稿人

Author1 photoNicholas Chase 曾经参与多家公司的网站开发,包括 Lucent Technologies、Sun Microsystems、Oracle 和 Tampa Bay Buccaneers。Nick 曾经做过高中物理教师、低放射性废弃设备管理员、在线科幻杂志的编辑、多媒体工程师、Oracle 教员以及一家交互通信公司的首席技术官。他出版了多部著作,包括 XML Primer Plus(Sams)。他还是 InterSection Unlimited 的合伙人,这家公司从事 Second Life 内容和应用程序的创建。在 Second Life 中,他的名字是 Chase Marellan。



2008 年 6 月 02 日

开始之前

本教程向您展示了如何使用 Ajax 创建基于 Web 的 IM 客户端,面向的读者是那些想要了解如何使用 Ajax 创建功能应用程序以及如何创建即时消息应用程序的开发人员。本教程使用了 Prototype JavaScript 库和 Jabber 即时消息服务器。您应该熟悉 Javascript、HTML 和 Java™ 编程。如果需要复习这些内容,请参看 参考资料 部分。

有关本教程

在本教程中,您将使用 Prototype Javascript 库及 Jabber 服务器创建一个基于 Web 的即时消息客户端。创建完成后,您将可以选择您的好友列表中的用户并能通过 Web 页面向传统 IM 客户端上的用户发送(或接收)消息。

在本教程中,您将了解:

  • 如何安装一个完整的即时消息解决方案
  • 如何创建即时消息 bot
  • 如何使用 Java 代码发送和接收即时消息
  • 如何使用 Jabber roster
  • 如何创建能自动更新的 Ajax Web 页面

所有这些最终会给您带来一个能将即时消息通信量转变为纯 HTTP 的应用程序,这样您就不会遇到防火墙问题。

先决条件

本教程使用了如下工具,所有这些工具均在第一部分设置:

  • Openfire:一种可与之通信的即时消息服务。Openfire 是一种开源 Jabber 服务器,可运行于 Windows® 操作系统、MacOs 或 Linux® 之上。
  • Spark:一种即时消息客户端,可用来查看状态。Spark 是 Openfire 人员提供的一种开源 Jabber 客户端。
  • Smack:一种与 Jabber 服务器通信的方式。Smack Java API 提供了您所需要的全部功能。
  • Prototype:可用来创建和管理 Ajax 应用程序的 Prototype JavaScript 库。
  • Apache Tomcat:一种 Java Web 应用服务器,能够运行应用程序所需的 servlet。
  • Eclipse 或另外的 Java 环境:您将在本教程中构建 Java 应用程序,您尽可以使用 Java IDE,比如 Eclipse,它让您能够方便地直接在应用服务器上进行开发。或者也可以从 http://java.sun.com 下载 Java SDK。

进行准备

本项目涉及了很多不同的块(正如 先决条件 中所看到的),让我们先来看看它们是如何协同工作的。

将要实现的目标

即时消息将世界联系的更紧密了 — 或者至少压缩了实现诸多事情所需的时间。在很多情况下,这是件好事。但在某些情况下,却是个问题,因为出于安全性方面的考虑,很多环境都禁止使用即时消息。

所幸的是,这一问题通过将即时消息通信量转变为纯 Web 通信量能够得到解决。在本教程中,我们将创建一个基于 Ajax 的 Web 客户端,用于进行即时消息传递。此页面包括 roster 或好友列表,并具有发起和维护与其中所列人员会话的能力。

简而言之,您可以从此 Web 页面发送消息并让其在好友的 IM 客户端上弹出,而且他们从其客户端上发回的消息也能显示在 Web 页面上供您查看。

重温 Ajax

如果已经开始阅读本教程,您可能已经非常熟悉 Ajax,不过为了以防万一我们还是先来温习一下。

Ajax 是一组技术和技巧的组合,可用来创建 Web 页面,其中的内容可分别更改。无需更新整个页面,只更改页面的一部分 — 比如实时交流 — 即可。这些信息大都通过 HTTP 来自于外部服务器。

请注意,Ajax 请求只可使用与发出请求的页面相同的协议、主机和端口,所以需要在 Web 服务器上托管此聊天客户端。

Prototype 简介

要进行所有这些 Ajax 调用是一件很复杂的事情:需要打开连接、侦听连接状态的更改、解析结果并相应进行动作。

所幸的是,现在已纪出现了很多简化此过程的库,最简单的一种方式是使用 Prototype.js。此库包含增强功能,可用来处理页面的文本对象模型( Document Object Model)— 让一般的 JavaScript 任务变得十分简单 — 以及 Ajax 对象。Ajax 对象可显式提供使用远端数据更新部分页面的能力。它甚至包括了经常性地更新信息的能力。

本教程对 JavaScript 和 Ajax 间的交互使用了 Prototype。

Jabber 简介

目前有很多不同的即时消息系统,有些可互操作,但是大部分不可以。Jabber 是面向消息传递应用程序的一种开放标准,这些消息传递应用程序大都包括发送和接收消息、好友列表管理(通过 roster)和在线检测(这样,您就可以知道好友是否登录)功能。

本教程使用了此标准的开源实现来将消息从 Web 页面传递给 Spark 即时消息客户端(或任何其他的 Jabber 客户端)并将消息发回。


逐一安装

在开始编写代码之前,需要在计算机上安装所有这些软件。

安装 Web 服务器

选择何种 Web 应用服务器对于本项目来说没有实质的影响,只要能够在其上轻松运行 Java servlet 即可。我发现安装 Apache Tomcat 5.5 并联合使用它和 Eclipse IDE 十分方便。这样一来,我就可以在一个很方便的位置部署我的 bot、servlet 和 Web 页面。

如果您是一名 Java 开发人员,那么很可能已经设置了与此类似的环境,如果还没有,可以参考如下的这些安装步骤:

  1. 下载 Apache Tomcat 5.5.x。如果使用的是 Windows 操作系统,就会发现最为简单的方式是使用 Windows 安装程序。不管何种情况,均需要核心库。
  2. 运行安装程序,或将库解压缩到一个方便的目录(最好路径内不要有空格)。
  3. 下载并安装 Eclipse 3.3。
  4. 创建一个新的 Dynamic Web 项目。
  5. 右键单击此项目并创建一个新的 servlet。在这个阶段名称并不重要。
  6. 右键单击此 servlet 并选择 Run As > Run on server
  7. 选择安装一个新的服务器并遵照指导添加刚刚创建的那个 Tomcat 安装。一旦新服务器创建完毕,右键单击此项目并删除它。
  8. 之后,当运行真正的 servlet 时,选择使用现有的服务器并选择您刚刚创建的那个服务器。

安装 Openfire

上述操作完成后,就可以开始安装特定于本项目的程序了。下载 Openfire 安装程序并按如下步骤操作:

  1. 启动这个可执行程序。
  2. 选择一种语言。
  3. 单击 Next
  4. 接受许可协议并单击 Next
  5. 选择一个位置并单击 Next
  6. 如果使用的是 Windows,选择 Start 菜单文件夹并单击 Next
  7. 选择完整运行 Openfire 并单击 Finish

该过程将 Openfire 作为一种简单的应用程序安装;安装完成后,还会弹出一个控制窗口。如果愿意的话,也可以将其作为服务安装以便在启动计算机时启动它。

配置 Openfire

当第一次运行 Openfire 时,必须先决定它将如何运行。这个控制窗口应该如图 1 所示:

图 1. Openfire 控制窗口
Openfire 控制窗口

请注意服务器已经运行,但还需要进行配置。单击 Launch Admin 来在浏览器内启动管理应用程序并按如下步骤操作:

  1. 选择一种语言并单击 Continue
  2. 设置机器的主机名以及想要使用的端口,如图 2 所示。单击 Continue
    图 2. 设置服务器的主机名和管理端口
    设置服务器的主机名和管理端口
  3. Openfire 需要一个数据库来存储帐号和消息信息。可以使用自己创建的数据库,但对于像本项目这样简单的应用程序,最好是使用嵌入的数据库,如图 3 所示:
    图 3. 选择一种数据库
    选择一种数据库
  4. 现在,需要告知 Openfire 想要如何存储用户信息。默认的方式是使用标准数据库表,但也可以以 LDAP 目录运行 Openfire,如图 4 所示。除非有合理的原因,否则请选择使用默认方法。
    图 4. 决定如何存储用户
    决定如何存储用户
  5. 接下来,设置管理员帐号,如图 5 所示。请务必注意所输入的密码;如果密码丢失,需要重新进行安装。
    图 5. 设置管理员帐号
    设置管理员帐号
  6. 在控制窗口,再次单击 Launch Admin 或将浏览器指向为管理服务所选的主机名和端口。

接下来,创建合适的用户帐号。

创建新帐号

为使此应用程序能够具有实际意义,需要至少两个 Jabber 帐号:一个是运行 Web 应用程序的 bot,另一个是代表交谈对象的测试帐号。

  1. 使用刚刚创建的帐号登录,用户名为 admin,密码为您输入的内容。开始页上有大量有关此服务器的信息,如图 6 所示:
    图 6. 管理控制台
    管理控制台
  2. 单击页面顶部的 Users/Groups 选项卡。
  3. 在左侧栏单击 Create new user,如图 7 所示:
    图 7. 创建新帐号
    创建新帐号
  4. 输入有关此主帐号的合适信息。此帐号是代表您的帐号,而且其 Jabber 地址就是您选择的用户名,所在的服务器也是您所选择的服务器。比如,如果服务器域名为 example.com,而所输入的用户名为 nick,则 Jabber 地址将会是 nick@example.com
  5. 单击 Create & Create Another
  6. 对两三个代表谈话对象的额外帐号执行相同的步骤。请至少记住一个用户名和密码;需要使用它登录。

现在让我们设置 IM 客户端。

安装 Spark

如果已经安装了另外一个 Jabber 客户端,可以忽略此步骤。否则,请遵照这些步骤来安装 Spark 即时消息客户端:

  1. 启动 Spark 可执行程序。
  2. 单击 Next 开始安装。
  3. 选择安装位置并单击 Next
  4. 如果使用的是 Windows 操作系统,就选择 Start 菜单文件夹并单击 Next
  5. 决定是否创建桌面和快速启动图标,单击 Next
  6. 选择安装完成后运行应用程序。
  7. 安装结束,客户端弹出时,如图 8 所示,输入在管理页上创建的用户信息:
    图 8. Spark 客户端
    Spark 客户端

单击 Login 登录到 Jabber 服务器。

要想真正地测试服务器安装,需要在不同位置创建第二个 Spark 安装并登录到所创建的另一个帐号。

安装 Smack

要构建 Jabber 应用程序,需要能够从应用程序访问 Smack API 类。在需要的时候配置应用程序或按如下步骤向 Eclipse Dynamic Web 项目添加 Jabber 支持:

  1. 下载并解压缩 Smack API 包。
  2. 右键单击此项目并选择 Build Path > Add External Libraries
  3. 导航到在其中解压缩 *.jar 文件的目录。
  4. 按 Shift 并单击选择所有四个 *.jar 文件。
  5. 单击 Open
  6. 展开 project > Web Content > WEB-INF
  7. 右键单击 lib 并选择 Import
  8. 单击 General > File System > Next
  9. 单击 Browse。选择具有 *.jar 文件的文件夹并单击 Open
  10. 选择所有四个 *.jar 文件旁边的复选框。确保选择了 Create selected folders only。 单击 Finish

上述处理让 Smack 类既可在开发环境中可用,也可在 Web 应用程序内可用。

安装 Prototype

Prototype JavaScript 库的设置异常简单。它是单一一个 JavaScript 文件,可以存储在单独的一个目录,也可以保存在与 Web 页面相同的目录下。在本教程中,我们采用了后一种方式。

  1. 将此文件下载到硬盘。
  2. 右键单击项目的 WebContent 文件夹并选择 Import
  3. 单击 General > File System > Next
  4. 单击 Browse。选择包含可下载的 *.js 文件的文件夹并单击 Open
  5. 选择 prototype.js 文件旁边的复选框。确保选中了 Create selected folders only。 单击 Finish

安装了所有这些必需的程序之后,就可以开始构建了。


基础 bot

现在,可以开始着手进行构建了。整个系统的核心是 “bot”,一个行为类似用户的 Java 类。它可以登录服务器、发送消息和接收消息。让我们先来创建 bot。

创建 bot

bot 本身是一个简单的 Java 类,具有若干导入的类和包以供日后使用。创建一个新文件,将其命名为 ChatBot.java(参见清单 1):

清单 1. 基础 bot
import java.util.Hashtable;
import java.util.Iterator;

import org.jivesoftware.smack.*;
import org.jivesoftware.smack.packet.Message;

public class ChatBot { 

    public static void main (String args[]){

    }

}

HashtableIterator 类用来跟踪会话。使用 org.jivesoftware.smack 包与 Jabber 服务器进行交互。

在线程内运行 bot

此 bot 需要连续运行,所以需要在线程内运行它(参见清单 2):

清单 2. 在线程内运行 bot
import java.util.Hashtable;
import java.util.Iterator;

import org.jivesoftware.smack.*;
import org.jivesoftware.smack.packet.Message;

public class ChatBot implements Runnable { 

    public static void main (String args[]){

        ChatBot test = new ChatBot();
        test.go();

    }

    public void run() {

        Thread current = Thread.currentThread();
        while (botThread == current){
            try {
                Thread.sleep(2000);
            } catch (InterruptedException e){
                //Do nothing, this is just the program ending.
            }
        }
    }

    Thread botThread;

    String queue = "start";

    public void go(){

        if (botThread == null) { 
            botThread = new Thread(this, "IMThread");
            botThread.start();
        }

    }
}

首先,需要为所有线程中的应用程序实现 Runnable 接口。在创建了新对象之后,运行 go() 方法来创建和启动线程本身,此线程又会启动 run() 方法。

现在让我们连接到服务器。

连接到 Jabber

接下来,需要使用之前创建的帐号连接到 Jabber 服务器(参见清单 3):

清单 3. 连接到 Jabber 服务器
....
public class ChatBot implements Runnable { 

    public static void main (String args[]){

        MessageTester test = new MessageTester();
        test.go();
        test.startNew();

    }

    public void stop(){
        conn.disconnect();
    }

....

    XMPPConnection conn;

    public void startNew(){

        try{
            conn = new XMPPConnection("myjabberserver.net");
            conn.connect();
            conn.login("myprimaryaccount", "mypassword");


        } catch (Exception e){
            System.out.println("StartNew Exception");
            e.printStackTrace();
        }

        System.out.println("Done.");
    }

}

一旦创建了线程,就可以继续并登录到 Jabber 服务器。由于将来需要访问该连接,所以需要让 XMPPConnection 对象成为一个全局变量。

当然,建立了连接,还要断开连接;如果停止线程,需要使用线程的内置 stop() 方法来断开连接。

创建 Chat

接下来,需要创建谈话本身。每个谈话都会发生在 Chat 对象的上下文。Chat 可发送消息到另一个人,而且还包含了响应对方所说的某些内容的功能。

最后,还需要跟踪多个 Chat 对象以及和其进行的交谈,但目前,我们只开启一个谈话(参见清单 4):

清单 4. 创建单个谈话
....
public class ChatBot implements Runnable { 

    Hashtable<String, String> chatQueues = new Hashtable<String, 
	String>(); // Holds the actual queues
    Hashtable<String, Chat> chatSessions = new Hashtable<String, Chat>();
	// Holds the Chat objects

....
    XMPPConnection conn;

    private void createConversation(String target){
        try {
            chatQueues.put(target, "");

            Chat chat = conn.getChatManager().createChat(target,
                    new MessageListener() {
                public void processMessage(Chat chat, Message message)  
                      {} 
            }
            ); 

            chatSessions.put(target, chat);    
        } catch (Exception e){
            System.out.println("Create Conversation Exception");
            e.printStackTrace();
        }
    }

    public void startNew(){

        try{
            conn = new XMPPConnection("myjabberserver.net");
            conn.connect();
            conn.login("myprimaryaccount", "mypassword");

createConversation("myfriend@myjabberserver.net");
            


        } catch (Exception e){
            System.out.println("StartNew Exception");
            e.printStackTrace();
        }

        System.out.println("Done.");
    }

}

从底部开始,当创建对象、登录之后,就需要创建新谈话,并提供给它您与之交流的那个人的用户名。

谈话的创建包含两类动作:与 Chat 本身相关的动作和与应用程序相关的动作。就 Chat 本身而言,需要用两条信息创建它。第一个是您想要与之交流的帐号的用户名。第二个是 MessageListener 对象,该对象能告知 Chat 在从帐户收到信息时该如何做。

所幸的是,可以内联创建 MessageListener。在本例中,将 processMessage() 这一相关方法留为空白;稍候再做处理。

发送消息

有了相关的 Chat 对象后,发送信息十分简单(参见清单 5):

清单 5. 发送消息
....
    public static void main (String args[]){

        ChatBot test = new ChatBot();
        test.go();
        test.startNew();
        test.sendMessage("myFriend@myjabberserver.net", "Hey there!");

    }
....
    XMPPConnection conn;

    public void sendMessage(String targetUser, String theMessage){
        Chat theChat = (Chat)chatSessions.get(targetUser);
        try {
            theChat.sendMessage(theMessage);
        } catch (Exception e){
            e.printStackTrace();
        }
    }

    private void createConversation(String target){
....

这里,向 main() 方法添加了一条语句,告知此对象一旦完成了自身初始化,它就应该发送一条文本消息给 myfriend@myjabberserver.net,地址与之前初始 Chat 所用的相同。发送后,bot 的 sendMessage() 方法就会寻找相关的 Chat 对象 —,记得么,最初是基于地址将其添加到 Hashtable 的。一旦找到了 Chat 对象,它就可以发送消息。

若要测试,可以使用 Spark 登录到发送消息的那个帐号并运行应用程序。应该可以看到弹出一个消息窗口,其上显示文本 “Hey there!”。

接收消息

但如果好友回发消息又该如何呢?消息会回到 Chat,然后呢?还不尽然。我们需要告诉 MessageListenerprocessMessage() 方法去处理此事,如清单 6 所示:

清单 6. 接收消息
....
    private void createConversation(String target){
        try {
            chatQueues.put(target, "");

            Chat chat = conn.getChatManager().createChat(target,
                    new MessageListener() {
                public void processMessage(Chat chat, Message message) {
                    try {
                        chat.sendMessage("Got "+message.getBody());}               
                    catch (Exception e){ 
                        e.printStackTrace();
                    }

                    String currentQueue = chatQueues.get(chat.getParticipant());
                    chatQueues.put(chat.getParticipant(), currentQueue 
					  + "<br />" + message.getBody());
                    S ystem.out.println(chat.getParticipant() + " said " + 
					  message.getBody());
                } 
            }
            ); 

            chatSessions.put(target, chat);    
        } catch (Exception e){
            System.out.println("Create Conversation Exception");
            e.printStackTrace();
        }
    }
....

现在,当用户从 Spark(或任何其他客户端)发送消息时,会发生两件事情。首先,检索消息主体 — 他/她究竟说了什么 — 并将其发送回给他/她。当然,这不是正常的会话协议,但它会让您在构建应用程序的时候知道收听者的工作。

第二,基于发出消息的那个用户(“参与者”)从队列检索应用程序的当前文本、向其添加当前消息并将其放回队列。

最后, 同样,还是可以查看其工作、输出一个语句说明所发生的事情。

保存、编译并运行此应用程序。这次,当谈话窗口弹出时,试着将文本发回。应该可以在谈话窗口本身以及 Java 控制台看到这样做的效果。

现在我们需要将此转变成一个 Web 应用程序。


创建 Web 应用程序

下一步是创建可以访问这个 bot 的 Web 应用程序。在本例中,该 Web 应用程序包含单个 servlet,可以将消息传递给 bot 和由 bot 传回。当用户告知 servlet 发送消息时,servlet 会将消息传递给 bot 的 sendMessage() 方法。当用户告知 servlet,他或她想要接收消息时,servlet 会将消息传递给 bot 的 getMessages() 方法。

创建 servlet

首先创建一个新 servlet,称为 ChatServlet。在 Eclipse,右键单击该项目并选择 New > Servlet

得到的 servlet 非常典型(参见清单 7):

清单 7. 基本的 servlet
import java.io.IOException;
import javax.servlet.ServletException;
import javax.servlet.http.HttpServletRequest;
import javax.servlet.http.HttpServletResponse;

public class ChatServlet 
                extends javax.servlet.http.HttpServlet 
                implements javax.servlet.Servlet {

    protected void doGet(HttpServletRequest request, 
                         HttpServletResponse response) 
              throws ServletException, IOException {
        
        
    }      

    protected void doPost(HttpServletRequest request, 
                          HttpServletResponse response) 
               throws ServletException, IOException {
    }                 
}

在实际的应用程序中,很可能使用 POST 方法;为了简化开发,我们将使用 GET,但前提是相同的。

现在让我们集成此 bot。

实例化此 bot

每次用户调用此 servlet 时,都需要检索此 bot 的相同实例,所以需要创建一个静态实例(参见清单 8):

清单 8. 实例化此 bot
....
 public class ChatServlet extends javax.servlet.http.HttpServlet 
                                 implements javax.servlet.Servlet {

   private static ChatBot bot = null;

   public ChatServlet() {
        super();
        
        bot = new ChatBot();
        bot.go();
        bot.startNew();

    }       
    
    protected void doGet(HttpServletRequest request, 
....

首次调用此 servlet 时,会执行构造函数。在本例中,该构造函数实例化静态 ChatBot 对象,bot。它还启动线程,与之前在主方法中所做的无异。

现在,每次调用 servlet 时,都会得到单一一个 bot 实例(构造函数只在第一次调用 servlet 时运行,所以不会有任何的重复)。

发送消息

有了 servlet 之后,就可以使用它来开始发送消息。方法是使用 GET 请求中的参数(参见清单 9):

清单 9. 发送消息
....
    protected void doGet(HttpServletRequest request, 
	  HttpServletResponse response) throws ServletException, IOException {
        
        if (request.getParameter("target") != null 
                && request.getParameter("message") != null){
            
            bot.sendMessage(request.getParameter("target").toString(), 
                           request.getParameter("message").toString());
            
        }
        
    }      
....

当消息到来时,如果该消息具备目标和消息参数,就可以认为用户想要发送消息,也就可以使用这些参数发送消息。这与从主方法对其进行调用是一样的。

若要测试,可以打开浏览器并将其指向: http://yourserver/proj/ChatServlet?target=myfriend@myserver.net&message=Hi!

当然,请确保在 URL 内放入您的具体信息,但您应该可以看到消息在您的 IM 客户端上弹出。

获得消息

还可以使用 servlet 检索消息,但消息不出现在控制台内,而是出现在浏览器内。要实现此目的,首先需要创建一种方法来检索这个合适的队列(参见清单 10):

清单 10. 在 ChatBot.java 获得消息
....

    public String getMessages(String targetUser){
        String queue = chatQueues.get(targetUser).toString();
        chatQueues.put(targetUser, "");
        return queue;        
    }

    private void createConversation(String target){
....

此方法十分直观 — 检索当前队列的文本、清除此队列并返回文本(请记住此队列,如 chat 会话一样,都是通过将地址作为它的键进行存储的)。一旦它就绪后,就可以从 servlet 调用它(参见清单 11):

清单 11. 在 ChatServlet.java 获得消息
....
    protected void doGet(HttpServletRequest request, HttpServletResponse response) 
	       throws ServletException, IOException {
        
        if (request.getParameter("getMessages") != null){
            response.getWriter().print(
               bot.getMessages(
                    request.getParameter("getMessages").toString()));
        }             
....

在本例中,servlet 寻找 getMessages 参数,该参数保存所请求队列的用户名。如果它存在,servlet 会获得消息(使用 bot)并输出到页面。若要测试,可以保存更改并重启服务器(这样就得到了一个新的 bot 实例)。试着发送消息以使窗口弹出,然后从 IM 客户端发送几行文本。

之后,可以用 URL 请求此队列,比如: http://yourserver/proj/ChatServlet?getMessages=myfriend@myserver.net

应该可以看到队列出现在浏览器窗口。

现在,需要将此功能合并到实际的 Web 客户端。


创建 Web 页面

现在具备了发送和接收消息的能力之后,就可以制作 Web 客户端了。

基本的 Web 客户端布局

我们从基本的布局着手。现在已经存在一些 Ajax 框架,可用来创建选项卡和其他漂亮的用户界面,而且最终,您很可能还会使用到其中的一个,但我们还是侧重基本功能。

我们先来创建一个具有四个基本区域的 Web 页面(参见清单 12):

清单 12. 基本 ChatPage.html
<html>
<head>
</head>
<body>

<h1>Chat Page</h1>

<table>
<tr>
   <td colspan="2">
     <div id="nameTag">NameTag</div>
   </td>
</tr>
<tr>
   <td style="width: 75%">
        <div style="border: 1px solid black; width: 100%; height: 500px;"
		 id="currentChat"></div>
   </td>
   <td>
     <div style="border: 1px solid black; width: 200px; height: 500px;" 
	 id="roster"></div>
   </td>
</tr>
<tr>
   <td colspan="2">
     <form id="chatForm" style="clear: left;">
        <input type="text" id="newChatText" style="width: 80%;" />
        <input type="button" id="newChatButton" value="Send" />
     </form>
   </td>
</tr>
</table>

</body>
</html>

没错,我有点循规蹈矩,我使用表来进行页面布局;不过它还是最迅速、最容易和最可靠的方式。重要的是我们有四个部分:顶部的 nameTag 分区;左侧保存会话的 currentChat div;保存好友列表的 roster div(在下一章节对此再做处理);以及底部表单,此表单包括可以在其中键入注释的文本字段。

结果应该如图 9 所示:

图 9. 空白页
空白页

现在让我们配置此页。

设立用户

首先,设立想要与之交谈的用户(参见清单 13):

清单 13. 设立用户
<html>
<head>

<script type="text/javascript" src="prototype.js" ></script>

<script type="text/javascript">

var targetName = "Dafyd Llewellyn";
var targetUser = "myfriend@myserver.net";

function populateForm(){

   $('nameTag').innerHTML = targetName;

}

</script>

</head>
<body onload="populateForm()">

<h1>Chat Page</h1>

<table>
<tr>
   <td colspan="2">
     <div id="nameTag">NameTag</div>
   </td>
</tr>
....

从顶部开始,包含进一个对 Prototype Javascript 库的引用;此库不仅可用来执行 Ajax 请求,还可以用于 DOM 操作。

比如,若想将 nameTag div 的 innerHTML 属性设为 targetName,可以不必借助 DOM (例如,document.getElementById('nameTag')),而是使用 Prototype 的美元符号 ($) 来直接请求对该元素的引用。

页面加载时,调用此函数,在刷新页面时,将会看到名称在页面顶部弹出。

填充聊天窗口

下一步是显示 currentChat div 内的所有现存聊天 — 您的聊天队列 —(参见清单 14):

清单 14. 填充聊天窗口
....
function populateForm(){

   new Ajax.Updater('currentChat', 'ChatServlet',
     {
       method: 'get',
       parameters: {getMessages: targetUser}
     });

   $('nameTag').innerHTML = targetName;

}
....

这里,使用 Prototype 的 Ajax 类来更新现有的 div — 在本例中是 currentChat— 用 Web 请求的内容。该请求是之前测试 getMessages() 方法时所用的相同的那个请求。指定的 URL(ChatServlet)和参数可用来调用此请求。这些参数包括方法(本例中是 get,但也可以是 post)以及针对此请求本身的参数。

在 IM 客户端内输入某些文本并刷新此 Web 页面以观其效。再输入一些文本并刷新页面以查看新文本。

这能工作,但很显然不是最好的方式。

自动更新聊天

真正想要的是页面能够用最新的信息不断刷新 currentChat 窗口。使用 PeriodicalUpdater 可实现此目的(参见清单 15):

清单 15. 持续更新此窗口
....
function populateForm(){

   new Ajax.PeriodicalUpdater('currentChat', 'ChatServlet',
     {
       method: 'get',
       insertion: Insertion.Bottom,
       frequency: 3,
       parameters: {getMessages: targetUser}
     });

   $('nameTag').innerHTML = targetName;

}
....

这样,就无需一次更新页面,而是能够创建 PeriodicalUpdater,由它按指定频率(在本例中是每隔 3 秒)执行更新。这里,您还可以看到额外的参数 insertion,它可以指定如果有文本到来,它会被添加到底部,而不是替换现有的聊天(您当然也可以将其添加到顶部)。

刷新页面并周期式地在 IM 窗口键入聊天。注意它会自动出现。

现在,让我们看看发送消息。

发送消息

通过底部的表单发送消息(参见清单 16):

清单 16. 发送消息
....
function sendMessage(){

   new Ajax.Request('ChatServlet', {
     method: 'get',
     parameters: {target: targetUser, message: $('newChatText').getValue()}
     });

}

</script>
....
     <form id="chatForm" style="clear: left;" onsubmit="sendMessage()">
        <input type="text" id="newChatText" style="width: 80%;" />
        <input type="button" id="newChatButton" value="Send" 
		                                              onclick="sendMessage()" />
     </form>
....

现在,当用户单击 Send 按钮或通过按下 Enter 提交表单时,sendMessage() 函数都会使用 Request 对象调用 servlet。在本例中,无需在意响应行为,所以无需使用 Updater;Request 完全可以完成此任务。在这里,为其传递当前 targetUser 的用户名以及 newChatText 字段的内容,正如 Prototype 检索的那样。

试着发送消息,并请注意它是否会在 IM 客户端弹出。

清除表单

即使在发送消息之后,文本还是处于文本字段。必须对此采取相应的措施;即需要清除该字段(参见清单 17):

清单 17. 清除表单
....
function sendMessage(){

   new Ajax.Request('ChatServlet', {
     method: 'get',
     parameters: {target: targetUser, message: $('newChatText').getValue()}
     });

   $('newChatText').value = "";

}
....

美元符号带来的是对某元素的引用,但尽管如此,还是需要将值属性设置为空字符串。请注意,这里所设置的是值,而之前处理 nameTag div 时所设置的是 innerHTML 属性。这是因为在最内部,分别处理的还是输入元素和 div。

向队列添加注释

至此,就可以进行交谈了,但读起来还不是很连贯,这是因为页面上显示的只有好友的对话。要解决这一问题,可以向队列添加注释,然后再将其发送给 IM 客户端,这可以通过向 ChatBot.java 类文件做简单的修改加以实现(参见清单 18):

清单 18. 向队列添加注释
....
    public void sendMessage(String targetUser, String theMessage){
        Chat theChat = (Chat)chatSessions.get(targetUser);
        String theQueue = (String)chatQueues.get(targetUser);
        try {
            theChat.sendMessage(theMessage);
            chatQueues.put(targetUser, theQueue + "<br />You: "+theMessage);
        } catch (Exception e){
            e.printStackTrace();
        }
    }
....

它所属的对话是已知的,所以您就可以从合适的队列中获得文本。一旦完成,所需做的就只是将消息添加到这个字符串,并加上注释说明是您所说的内容,并将文本发送回队列。

若想测试,可以在 Web 页面和 IM 客户端之间进行谈话。请注意注释现在出现在这两个地方。

对于单一对话,这可以很好地工作,而且通过打开多个页面,也可以与多人聊天,但有一种更为简单的方法:通过 roster。


处理 roster

每个即时消息平台都有一些 “好友列表”,即您经常与之交谈的用户列表。它通常出现在单独的窗口或邻近当前对话。借助 roster 可以更方便地发起对话,而且也可以让即时消息客户端得以告诉您谁在线谁没有在线。

在本教程中,您可以使用 roster 来协助管理对话。其思想是 roster 在右侧栏显示,而通过单击想要与之交谈的对象的名字能够在对话间切换。

向 roster 添加

第一步是构建 roster。如果使用的是传统的 IM 客户端,这将比较简单;通常会有某些按钮,可以用来添加当前聊天、作为好友参与。

在本例中,必须使用 bot 向用户的 roster 中添加。所幸的是,这个过程十分简单。首先,向 bot 本身添加此功能,方法是对 ChatBot.java 做如下更改(参见清单 19):

清单 19. 向 roster 添加
....
    public void addToRoster(String targetUser, String targetName){
        Roster roster = conn.getRoster();
        try {
            roster.createEntry(targetUser, targetName, null);
            createConversation(targetUser);
        } catch (XMPPException e){
            e.printStackTrace();
        }
    }
....

此方法接受想要添加到 roster 的用户地址和名字。请注意重要的是地址;名字可用作 “显示名称”,正如本教程中所做的那样。

首先,让用户的 roster 登录到当前连接(我假定这就是 bot 帐号)。有了 roster,所需做的就只是创建新的条目。

请注意,之后,为这个新添加的条目创建了一个对话。严格来讲,在本教程中这不是必需的,因为在应用程序运行时,并没有向 roster 中动态添加用户,我之所以这么做是为了举例说明。

现在需要向 ChatServlet 添加此项功能(参见清单 20):

清单 20. 向 servlet 添加 roster 功能
....
    protected void doGet(HttpServletRequest request, 
                         HttpServletResponse response) 
            throws ServletException, IOException {
        
        if (request.getParameter("addToRoster") != null){
            bot.addToRoster(
                 request.getParameter("addToRoster").toString(), 
                 request.getParameter("rosterName").toString());

        }
....

这里所做的是检测想要向 roster 中添加名称的用户并为 bot 提供合适的信息。通过调用如下所示的 URL 可以向 roster 添加想要与之交流的任意多的好友:

 <a href="http://yourserver/proj/ChatServlet?addToRoster=myfriend@myserver.net&
  rosterName=My+Friend">http://yourserver/proj/ChatServlet?addToRoster=
  myfriend@myserver.net&rosterName=My+Friend</a>

现在需要将 roster 信息取回。

获取 roster

向 roster 添加了好友之后,需要查看谁在其列。这是聊天 Web 页面所需的。首先向 ChatBot 类添加新方法(参见清单 21):

清单 21. 获取 roster
....
    public String getRoster(){
        Roster roster = conn.getRoster();
        String returnStr = "";
        Iterator<RosterEntry> iter = roster.getEntries().iterator();
        while (iter.hasNext()) {
            RosterEntry entry = (RosterEntry) iter.next();
            returnStr = returnStr + entry.getName() + "<br />";
        }
        return returnStr;
    }
....

这里仍然还是为当前连接检索 roster。getEntries() 方法返回所有条目的 Enumeration,使用 Iterator 可以访问到此 Enumeration。对于每个条目,您都会向 returnStr 变量添加名称和一个分行符以便将它输出到 Web 时能正确显示。

当然,需要能够从 servlet 检索到此 roster(参见清单 22):

清单 22. 从 servlet 检索此 roster
....
    protected void doGet(HttpServletRequest request, 
                         HttpServletResponse response) 
                 throws ServletException, IOException {
        
....
        if (request.getParameter("getRoster") != null){
            response.getWriter().println(bot.getRoster());
        }
....

若要测试,可以从浏览器发起对它的请求。getRoster 参数的值为多少并不重要;但它必须存在。

从 roster 中删除

为了全面起见,需要了解如何从 roster 删除某人(比如,不再想与这些人交谈),如清单 23 所示:

清单 23. 从 roster 删除某人
....
    public void removeFromRoster(String targetUser){
        Roster roster = conn.getRoster();
        try {
            RosterEntry entry = roster.getEntry(targetUser);
            roster.removeEntry(entry);

            chatSessions.remove(targetUser);
            chatQueues.remove(targetUser);

        } catch (XMPPException e){
            e.printStackTrace();
        }
    }
....

这里最重要的是一旦您获得了 Roster 对象本身,需要首先找到 RosterEntry,然后才能删除它。否则,删除的只是此用户的对话。同样,如果在应用程序运行期间,如果不是动态添加和删除 roster 条目,这也不是必需的。

当然,还需要向 servlet 添加此功能(参见清单 24):

清单 24. 从 servlet 删除 roster 条目
....
    protected void doGet(HttpServletRequest request, 
                         HttpServletResponse response) 
                   throws ServletException, IOException {
        
        if (request.getParameter("removeFromRoster") != null){
            bot.removeFromRoster(
               request.getParameter("removeFromRoster").toString());
        }
....

在添加用户的情况下,参数的值很重要,应该包含所针对的用户的地址。

现在让我们向此页面添加 roster。

显示 roster

Prototype 使得向页面添加 roster 变得异常简单(参见清单 25):

清单 25. 显示 roster
....
function populateForm(){
   new Ajax.Updater('roster', 'ChatServlet', {
     method: 'get',
     parameters: {getRoster: 'yes'}
   });
....

在填充页面时,请采取额外步骤调用此 servlet 以便获取 roster 并用它来更新 roster div。如前所述,getRoster 的值并不重要。如果保存和编译所有合适的文件并重启 servlet 引擎以期获得全新的 bot 实例,roster 就会出现在右侧 div,如图 10 所示:

图 10. roster
roster

但我们还有一个问题:我们没有现成的 Chat 对象给这些人。

为 roster 发起对话

要与某人对话,需要先有 Chat 对象。在 ChatBot,这发生于 createConversation() 方法,意味着需要为 roster 中的每个条目都要调用它(参见清单 26):

清单 26. 创建对话
....
    public void startNew(){

        try{
            conn = new XMPPConnection("myserver.net");
            conn.connect();
            conn.login("myprimaryaccount", "mypassword");

            Roster roster = conn.getRoster();
            Iterator<RosterEntry> iter = 
                       roster.getEntries().iterator();
            while (iter.hasNext()) {
                RosterEntry entry = (RosterEntry) iter.next();
                createConversation(entry.getUser());
            }


        } catch (Exception e){
            System.out.println("StartNew Exception");
            e.printStackTrace();
        }

        System.out.println("Done.");
    }
}

在首次实例化 servlet 内的 bot 时,都要调用 startNew() ,所以这是很好的一个运行 roster 内的每个条目并为其创建对话的地方。这也是发起原始对话的地方,所以变化不是很大。

选择新用户:向 roster 添加脚本

即使在 roster 上有了这些用户,如果没有办法告知 Web 客户端您想要与之交谈,用处也不会很大。要实现此目的,需要让页面在您单击用户名字的时候,能够切换到新用户。

实现这个目的的方法很多,包括将 roster 作为数组传递并动态地将每一个都添加到此文档或使用 Ajax 框架创建一个选项卡系统。在很多时候,越简单越好。

编辑 ChatBot 类以便向每个 roster 条目都添加事件处理程序(参见清单 27):

清单 27. 每个 roster 条目都添加事件处理程序
....
    public String getRoster(){
        Roster roster = conn.getRoster();
        String returnStr = "";
        Iterator<RosterEntry> iter = roster.getEntries().iterator();
        while (iter.hasNext()) {
            RosterEntry entry = (RosterEntry) iter.next();
            returnStr = returnStr + "<span id='"+entry.getUser()+
                   "' onclick='changeUser(\""+entry.getUser()+
                   "\", \""+entry.getName()+"\")'>"+entry.getName() + 
                   "</span><br />";
        }
....

上述代码看上去不太整齐,如果佐以 HTML,结果应该如清单 28 所示:

清单 28. 结果 HTML
<span id='myfriend@myserver.net' 
           onclick='changeUser("myfriend@myserver.net", "My Friend")'>
  My Friend
</span><br />

这里最重要的地方是 onclick 处理程序,它调用 changeUser() 函数,向它发送帐号和想要使用的显示名称。

现在,必须要在 Web 页面创建 changeUser() 函数。

选择新用户:更改 Web 页面

现在可以开始最后一阶段的操作了。所需做的就是在单击 roster 内的名字时更改 “活动” 用户(参见清单 29):

清单 29. 更改用户
....
<script type="text/javascript">

var targetName = "Dafyd Llewellyn";
var targetUser = "myfriend@myserver.net";

var updater;

function changeUser(newUser, newName){
   $('currentChat').value = "";
   targetName = newName;
   targetUser = newUser;

   $('nameTag').innerHTML = targetName;

   updater.stop();
   updater = new Ajax.PeriodicalUpdater('currentChat', 'ChatServlet',
     {
       method: 'get',
       insertion: Insertion.Bottom,
       frequency: 3,
       parameters: {getMessages: targetUser}
     });
  
}


function populateForm(){
   new Ajax.Updater('roster', 'ChatServlet', {
     method: 'get',
     parameters: {getRoster: 'yes'}
   });

   updater = new Ajax.PeriodicalUpdater('currentChat', 'ChatServlet',
     {
       method: 'get',
       insertion: Insertion.Bottom,
....

这里发生了几件事情,让我们从底部开始。首先,PeriodicalUpdater 为当前用户重复请求队列。如果打算更改此用户,就需要告知 PeriodicalUpdater 停止 ping 旧地址,方法是创建一个全局 updater 变量并 将其设置为新对象。然后,再向上看,在 changeUser() 函数,使用 updater 变量调用 PeriodicalUpdaterstop() 方法,之后用正确的信息创建一个新用户。

最后,需要清除当前聊天,将 targetUsertargetName 变量设为合适的值并将 nameTag div 更改为新名称。

若要测试,可以刷新页面并在各种 roster 条目上单击。能否看到 IM 客户端上的动作取决于您与之登录的用户是否是页面的当前用户。


结束语

您已经了解了如何通过使用 Jabber 和 Prototype 创建基于 Ajax 的即时消息客户端。您创建了一个接受和转发消息的简单 bot 并了解了如何使用 roster 来跟踪多个会话。您还学会了如何使用 Prototype 进行更简单的 DOM 处理以及如何轻松地一次或多次更新 Web 页面的某些部分。

当然,本教程中的练习没有创建生产应用程序。请记住,本练习没有任何安全性,而且谈话也不会保存;当选择新会话时,旧的会话就不在了。但我希望本练习能够为您提供一个很好的起点,由此,您可以决定如何进一步改进。


下载

描述名字大小
本教程的源代码wa-aj-imclient.zip4KB

参考资料

学习

获得产品和技术

  • Prototype 是一种 JavaScript 库,它引入了功能强大的函数来帮助简化 Ajax 编程。
  • Openfire 是一种可运行在 Windows、MacOs 或 Linux 上的开源 Jabber 服务器。
  • Spark 是 Openfire 厂商提供的一种开源 Jabber 客户端。
  • Smack Java API 提供了与 Jabber 服务器通信所需要的所有功能。
  • 在 IBM alphaWorks 查看最新的 Eclipse 技术下载
  • 下载 IBM 产品评估版 开始实践来自 DB2®、Lotus®、Rational®、Tivoli® 和 WebSphere® 的应用程序开发工具和中间件产品。

讨论

条评论

developerWorks: 登录

标有星(*)号的字段是必填字段。


需要一个 IBM ID?
忘记 IBM ID?


忘记密码?
更改您的密码

单击提交则表示您同意developerWorks 的条款和条件。 查看条款和条件

 


在您首次登录 developerWorks 时,会为您创建一份个人概要。您的个人概要中的信息(您的姓名、国家/地区,以及公司名称)是公开显示的,而且会随着您发布的任何内容一起显示,除非您选择隐藏您的公司名称。您可以随时更新您的 IBM 帐户。

所有提交的信息确保安全。

选择您的昵称



当您初次登录到 developerWorks 时,将会为您创建一份概要信息,您需要指定一个昵称。您的昵称将和您在 developerWorks 发布的内容显示在一起。

昵称长度在 3 至 31 个字符之间。 您的昵称在 developerWorks 社区中必须是唯一的,并且出于隐私保护的原因,不能是您的电子邮件地址。

标有星(*)号的字段是必填字段。

(昵称长度在 3 至 31 个字符之间)

单击提交则表示您同意developerWorks 的条款和条件。 查看条款和条件.

 


所有提交的信息确保安全。


static.content.url=http://www.ibm.com/developerworks/js/artrating/
SITE_ID=10
Zone=Web development, XML
ArticleID=311522
ArticleTitle=创建基于 Ajax 的 IM 客户端
publish-date=06022008