Internet 即时通信系统的设计与实现

Comments

为什么选择 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
图 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)
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。2
    图 2。3。3
    图 2。3。3
    图 2。3。3
  2. 客户端程序设计(图 2。3。4)

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

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

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

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

    图 2。3。4
    图 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。1
图 2。4。2
图 2。4。2
图 2。4。2

5 .系统扩展及补充说明

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


评论

添加或订阅评论,请先登录注册

static.content.url=http://www.ibm.com/developerworks/js/artrating/
SITE_ID=10
Zone=Java technology, Web development
ArticleID=52859
ArticleTitle=Internet 即时通信系统的设计与实现
publish-date=10162001