网络聊天室(Java)

摘要

本文阐述了基于Linux环境,Java语言实现的基本聊天室功能,涉及Linux下的Java 语言的Socket编程。以及Java语言的多线程编程。

 
关键字
Linux         Java                  Thread              Socket              Tcp
 
简介
开发背景
操作系统》和《计算机网络》的学习,使我能够有机会选择“基于Linux的网络聊天室的实现”这个课题项目,使自己课堂所学的理论能够联系实际,并且能够学习自己没有涉及过网络方面以及面向对象的基本思想,而且在编写聊天程序的过程中,也涉及到多线程的程序设计问题,这个概念在《操作系统》中已经学到,但是对于真正的语言应用却是第一次。Java语言的多线程编程提供了很好的基础类,可以很容易实现多线程的调用。以及Java语言的跨平台,使我有机会在Linux下编写程序,当然对于Linux下的Java编程和Windows下的Java编程,没有多少的不同,这些都是Java语言没有平台特有的特征带来的。
 
系统开发环境
Linux正以自由的精神席卷全球网络操作系统市场,而Java凭借其开放、先进的架构正迅速占领着高端软件领域。将这二者结合,便可通过Linux低廉的成本实现Java高级应用,在自由、高效的环境下充分发挥出Java的优势。因此,无论从成本还是性能上考虑,二者的结合都可谓是相得益彰。
例如,现在热门的服务器端脚本JSP的推荐实现就是Linux上的Tomcat,而与Jboss结合更是极佳的EJB平台。但是,Linux之所以未能在桌面应用等领域迅速普及,软件安装和设置复杂是一个重要原因。要在Linux下实现Java编程,其普通的环境设置可能令习惯了Windows的用户望而却步。其实,很多问题只需要简单的设置就能解决。
而对于本次课程项目的开发也正是两者的结合,尽管结合没有发挥各自的精髓,但是也能体会和感受到Java+Linux 的魅力。
 
技术概要
网络通信基本原理

Ø         TCP (Transmission Control Protocol)基础

 
数据传输协议允许创建和维护与远程计算机的连接。连接两台计算机就可彼此进行数据传输。如果创建客户应用程序,就必须知道服务器计算机名或者  IP  地址(RemoteHost  属性),还要知道进行 侦听 的端口(RemotePort  属性),然后调用  Connect  方法。如果创建服务器应用程序,就应设置一个收听端口(LocalPort  属性)并调用  Listen  方法。当客户计算机需要连接时就会发生  ConnectionRequest  事件。为了完成连接,可调用  ConnectionRequest  事件内的  Accept  方法。建立连接后,任何一方计算机都可以收发数据。为了发送数据,可调用  SendData  方法。当接收数据时会发生  DataArrival  事件。调用  DataArrival  事件内的  GetData  方法就可获取数据。
 

Ø         UDP(User Datagram Protocol) 基础

 
用户数据文报协议  (UDP)  是一个无连接协议。跟  TCP  的操作不同,计算机并不建立连接。另外  UDP  应用程序可以是客户机,也可以是服务器。
为了传输数据,首先要设置客户计算机的  LocalPort  属性。然后,服务器计算机只需将 
RemoteHost  设置为客户计算机的  Internet  地址,并将  RemotePort  属性设置为跟客户计算机的  LocalPort  属性相同的端口,并调用  SendData  方法来着手发送信息。于是,客户计算机使用DataArrival  事件内的  GetData  方法来获取已发送的信息。
 

Ø         Socket(Java)

 
套接字方式通信 (socket-based communication) 通过指派套接字实现程序自己的通信。套接字(Socket) 是一种抽象,为服务器和客户之间的通信提供方便。Java处理套接字通信的方式很像处理I/O操作,这样,程序对套接字进行读写就像读写文件一样容易。
 

Java支持流套接字(steam socket)和数据报套接字(datagram socket)。流套接字使用TCP协议(Transmission Control Protocol, 传输控制协议)进行数据的传输,而数据报套接字使用UDP协议(User Datagram Protocol, 用户数据报协议)。因为TCP能够探测丢失的数据传输并重新提交它们,因此传输的数据不会丢失,是可靠的。相比之下,UDP协议不能保证无损失传输。所以,采用TCP协议通信可以保证数据的正确传输。

 

Ø         客户/服务器模式

 
网络聊天室涉及的一个服务器端和N个客户端。客户向服务器发送请求,服务器对请求作出响应。客户尝试与服务器建立连接,服务器可以接受连接也可以拒绝连接。一旦连接建立起来,客户和服务器就可以通过套节字进行通信。
   客户开始工作时,服务器必须正在运行,等待客户的连接请求。创建服务器和客户所需要的语句如图1-1所示。

    

 
图1-1 服务器创建一个服务器套接字,与用户的连接一旦建立,就用客户套接字也客户保持连接
 
要建立服务器,需要创建一个服务器套接字,并把它附加到一个端口上,服务器通过这个端口监听连接请求。端口标识套接字上的TCP服务。编号在0到1023之间的端口用来为特权进程服务。
 
下面的语句创建一个服务器套接字server:

ServerSocket server = new ServerSocket(port);1

   

 
 

  [1] 创建了一个服务器套接字之后,服务器就能够使用下面的语句监听连接请求:

Socket connectToClient = server.accept();

   

 
 

 这条语句一直等待,直到客户连接到服务器套接字。客户发送下面的语句与服务器建立连接:

Socket connectToServer = new Socket(ServerName, port);

   

 
 

该语句用来打开一个套接字,以便客户程序能够与服务器进行通信。 ServerName是服务气的Internet主机名或IP地址。
服务器接受连接之后,就用与处理 I/O数据流相同的方式建立服务器与客户之间的通信。
要获得输入数据流和输出数据流,可以使用套接字对象中的 getInputStream()和getOutputstream()方法:

InputStream isFromServer = connectToServer.getInputStream();

OutputStream osToServer = connectToServer.getOutputSteam();
 
   

 
 
 

      InputStream 和 OutputSteam是用户读写字节。可以使用DataInputStream、DataOutputSteam、BufferReader 和 PrintWriter包装InputStream和OutputSteam,读取double、int、String之类的数据值。可以使用readLine方法读入一行数据,使用println向端口写入一行。

 
多线程技术编程基本原理
一个线程 (Thread) 是指程序中完成一个任务的有始有终的执行流。 Java 语言支持多个线程同时运行。如下图 1-2
 
 



 

 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 

图1-2 多个线程分享一个CPU
 
 
多线程可以使程序反应更快、交互性更强,并提高执行效率。 Java 对多线程程序设计提供更好的支持,包括内在地支持创建线程和锁定资源以避免冲突,解决了资源的共享冲突问题。
当程序运行时, Java 解释器为 main 方法开始一个线程。此时可以创建另外的线程。每一个线程都是一个对象,它的类实现 Runnable 接口或者扩展实现了 Runnable 接口的类。也可以通过扩展 Thread 类来实现 Runnable 接口来创建线程。 Thread 事实实现了 Runnable 接口。
 
 

Ø         线程有五种状态:新建、就绪、运行、阻塞、结束。

如图 1-3 所示:

图1-3一个线程处于其中的一个状态
 
 
à       新创建一个线程的时候,它进入“新建状态”。调用start方法启动线程后,它进入“就绪状态”。就绪态可以通过调用run方法实现到“运行状态”的转移。
à       如果给定的CPU时间用完,或者调用yield()方法可以使线程处于“就绪状态”
à       当线程执行结束,然后其自然应该进入“结束状态”。
à       当线程因为调用sleep()等方法,将进入“阻塞状态”。此状态还可以重新进入“就绪状态”,接着重新得到运行。
 

Ø         Thread类包括以下的几种控制方法:

 
public void run()方法,用来执行线程。用户线程类中必须覆盖该方法。
public void start()方法,它引起对run方法的调用。
public void stop()方法,结束一个线程
public void suspend()方法,挂起一个线程 [2]
public void resume()方法,唤醒一个线程 [3]

public static void sleep(long millis) throws InterruptedException 方法,可以将在运行的线程置为休眠状态,休眠时间为指定的毫秒。

public void interrupt()方法,中断正在运行的线程。
public static boolean isInterrupted()方法,测试线程是否被中断。
public boolean isAlive()方法,检查线程是不是处于运行态。
public void setPriority(int p)方法,设置方法的优先级。从1~10
public final void wait() throws InterruptedException 方法,将该线程置为暂停状态,等待另外一个线程的通知。
public final void notify()方法,唤醒一个等待的线程。
 
Linux下Java编程
本次课题项目采用的环境是:
 

¨         Linux Platform - J2SE(TM) and NetBeans(TM) IDE Bundle NB 4.1 / J2SE 5.0 Update 4 [4]

¨         Rat Hat Linux 9.0

 

Linux采用J2SENetBeans可以很容易的开发面向Linux的应用程序,可以移植到windows平台下运行。其中NetBean Sun公司开发的免费Java图形用户界面编辑器。(如图1-4)可以很轻松的实现界面的设计,它将控件以Swing awt JavaBean分类放置。

集成 Tomcat 5.0 加入对 XML,Structs 的支持。这其中感触最深的地方是,不能修改自动生成的组件初始代码。假如要用 ButtonGroup 就得自己去另写一个函数来初始化。感觉这样做,因为不能随意修改代码,能避免随意修改所导致的错误。但是,很多时候,我们真的要修改那部分代码反倒是件很麻烦的事了。
对于 window 平台有个比较不当的地方就是内存消耗太大。硬盘频繁访问。在 Linux 环境可以明显感觉到速度快了不少!
 

 
 
图1-4 windows 平台下的NetBeans运行界面 [5]
基于Linux的网络聊天室的具体实现
服务器端和客户端体系结构
根据通信的基本原理,不难分析服务器端与客户端的通信实现,以下是客户端和服务器端的交互流程,如图 1-5

 
图1-5 客户端和服务器端的基本流程
 
流程图的简要描述
Server Client 端通信主要是服务器端创建多个线程,生成多个 Socket 对不同的用户进行通信。服务器端和客户端通过消息命令字的方式进行消息确认。方式是在消息头加入“命令字”。
自定义命令字含义如下:
 
[MESSAGE] :表示接下来的一句话是消息
 
[NAME]:   表示接下来的一句话是名字
[FIRSTNAME] :用于程序逻辑控制,表示第一个
[SYSTEM]: 系统消息                                                
[Server exit!]   服务器退出
[WHISPERMESSAGE] :私聊控制字
 
[QUIT]      表示客户端退出聊天室
 
客户端:
客户端由两个类实现,一个是主类 ClientJFrame 另外一个是用于播放声音的 PlaySound 类。一下是客户端的类关系图,图 1-6

图1-6 Client端两个类,ClientJFrame中创建PlaySound实例来播放声音
 

Ø        

public void startConnect()
    {
        try
        {

            sock = new Socket(ipAddress, DEFAULT_PORT); //新建一个socket

            if(sock!=null)                                                                                            //连接成功

            {

                processMsg();

}

isFromServer=new BufferedReader();              //新建一个接收变量

osToServer = new PrintWriter();                                              //新建一个输出变量

osToServer.println("[NAME]" + name);                       //发名字

osToServer.flush();                                                                               //刷新数据缓冲
        }

        catch(IOException ex)

        {
        }

                       readThread=new Thread(this);                                                 //通过Runnable实现

        readThread.start();

}
  Client端通过方法startConnect(),尝试连接服务器端,其函数原型如下:

Ø         通过SendInformation()方法实现数据的发送,实现函数原型如下:

 

public void sendInformation()
{
    if(私聊)
    {

        osToServer.println("[WHISPERMESSAGE]" + message);//发送私聊信息

                         osToServer.flush();
    }
    else
    {

        osToServer.println("[MESSAGE]" + message);                   //发送群聊信息

        osToServer.flush();

    }
}
  
   

 
 
 
 
 
 
 
 
 
 
 
 
 
 

通过调用prinln()方法可以向端口写一句消息。
 

Ø             当程序退出,或者服务器退出,线程应该结束运行。客户端,通过重载Thread的run方法实现,函数的原型如下:

public void exit()
{
 try
{
 osToServer.println("[QUIT]");   //向服务器发送退出命令字

    osToServer.flush();

 }

 catch(Exception exc){}

 try
 {
    sock.close();                                                 //关闭socket

    isFromServer.close();          //输入缓冲关闭

    osToServer.close();            //输出缓冲关闭

 }
 catch(IOException ioe){}
 finally                                                                          //不管异常是否发生都将执行
 {

    System.exit(0);

 }
 }  
   

 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 

à       在退出应用程序之前,向服务器端发送[QUIT]命令字,实现聊天室的更新。

à       客户端退出要将socket关闭,要将输入和输出的数据流关闭。

à       最后将执行finally块程序,最后调用System的exit的函数退出应用程序。

 
 
 
服务器端
服务器端主要创建了 5 个类,其中 ServerJFrame 是主类, CommunicateThread 用于连接客户端, BroadcastThread 用与对消息的广播, WhisperThread 用于处理悄悄话, BroadcastName 用户广播当前在线的用户。图 1-7 显示了类中的方法和类的属性。
 

图1-7 Server端的类视图
 

Ø             Server相对与客户端更加的复杂,要主动监听客户端发送的连接请求,创建不同的线程,来应答客户的请求。创建的线程,接受客户发送的数据的处理。

 
Server创建了连接线程后,还必须创建广播线程,将每一个客户发送的消息广播出去,到每一个客户端。对于广播线程和数据接受和处理线程之间的资源共享问题,我采用了,有序运行的方法来消除死锁。即在用户发送来数据时,开启广播线程,对刚才的数据进行广播,在信息广播结束后,关闭广播线程。这样一前一后,就可以保证数据广播的正确性。
 
在server还需要处理的一件事,就是如何将私聊信息发送给指定的客户端。我采用的方式是,“用户名查找发送法”,可以比较快而准确的发送数据 [6] ,为了和群聊消息区分,我采用WhisperThread类单独给予处理。这样可以清晰的区分。
 
在客户端,还需要处理的一件事情就是,管理当前的在线用户。我使用一个堆栈 [7] 来管理用户,当用户来到时,就将用户名压入堆栈。当用户退出时,将用户的名字从中去除 [8]
      

Ø             服务端使用serverListen()函数开始监听端口,其函数原型如下:

private void serverListen()
{

      chatAcceptThread = new Thread(this);                    //创建一个监听线程

      chatAcceptThread.start();

      broadcastThread = new BroadcastThread(this); //创建广播线程

      broadcastThread.start();

}
   


      
      
      
      
      
      
      
      
      
      

Ø             ServerJFrame是通过Runnable接口来创建的线程。其run函数实现客户端的接受并创建一个新的线程:

public void run()
{

clients = new java.util.Vector();                //分配一个栈,用于存储用户线程

    clientsInfor = new java.util.Vector();  //用于存储用户名
    try
    {

       serverSock=new ServerSocket(DEFAULT_PORT); //新建一个Socket

    }

    catch(IOException e){}

    try
    {

        while(true)

        {

           Socket clientSock = serverSock.accept();

           CommunicateThread ct = new CommunicateThread(); //实例化通信类

           boolean addSucOrNot = clients.add(ct);            //将当前的进程压入堆栈                      

        }
     }

     catch(IOException e){}

}
   


      
      
      
      
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
      
在run函数中有一个while(true)程序块,表示这个进程将随应用进程一起存在,所以服务器端将不停的监听端口,当一个连接进入的时候,就通过CommunicateThread来处理当前连接的进程(客户端),然后,服务器主线程将监听下一个连接。

为了后面能够将数据广播出去,和实现私聊,必须要得到响应的线程,所以在向堆栈压入线程的时候,需要有一个变量(index)来指示线程, index 不会随着客户的退出而删除 [9] ,而是逐次累加,那么当客户退出时,要将此进程在堆栈中的位置设置为[EMPTY],来表示一个客户端已经退出,此时,服务器端要结束和这个客户端连接的线程。


Ø             那么当用户发来数据的时候,并且命令字为[MESSAGE]的时候,服务器需要将这条信息广播出去,这个由Broadcast来处理,其中的run函数原型如下:

public void run()
{
   try
   {
      while(true)
      {               
//若消息堆栈为空,或者没有当前信息需要发送

         boolean startBroadcast = chatFrame2.getBroadcastStart();

         if (!startBroadcast)

         {
            try

               Thread.sleep(500);                // 线程睡眠500毫秒后重新检测

            catch(InterruptedException ex){}

            continue;

         }

         int lengthOfChatClients = chatClients.size();          //线程个数

         for(int i=0; i < lengthOfChatClients; i++)   //对每个线程进行操作

         {

            if(chatClients.get(i).equals("[EMPTY]")) //若是退出进程

               continue;

            comThread1 = (CommunicateThread)chatClients.get(i);

            msgStack = comThread1.inforStack;

            int lengthOfMsgStack = msgStack.size(); //对消息堆栈进行广播

            for(int j=0; j<lengthOfMsgStack; j++)

            {

                string = (String)msgStack.get(j);

                broadcastInfor = string;

               broadcast("[MESSAGE]" + broadcastInfor);

                boolean temp = msgStack.removeElement(string);

            }
          }
          try
          {

                chatFrame2.stopBroadcast();                        //停止广播

                Thread.sleep(1000);

          }

          catch(InterruptedException ex) {}

        }
   }

            catch(Exception e){}

}
   

 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 

广播线程,主要是和接受消息线程达到同步,通过共享内存堆栈来实现数据的交互。然后广播线程需要解决的问题就是如何实现线程的数据广播,这是使用的是,调用创建的线程,然后调用CommunicateThread的通信进行数据的广播。其中名字的广播也是同样的一个道理。
 
执行效果演示
演示效果如下图所示 ( 1-8)
 

 
图1-8是windows下的运行效果,充分说明在Linux下开发的应用程序的可移植性,在Windows下运行无阻
 

Ø         程序可以实现公聊和私聊 [10] ,公聊在服务器端将加入聊天记录,私聊则只是发给指定用户,服务器端不保留聊天信息。

Ø         收到系统消息,和用户变化都会有声音提示。

Ø         完全可以单机来调试信息,也试过在Linux下运行服务器端,在Windows下使用客户端进行访问,访问方式没有区别,通信也没有故障。

Ø         当服务器退出时,或者说用户端失去服务器连接时,用户将需要重新连接,当然也可以实现超时退出的方式,这样可以实现重新连接。

Ø         可扩展功能:系统可以选择需要发送的系统消息的对象,这样可以使系统消息发送更加灵活。

Ø         用户可以通过右边的list得到当前的在线用户的状况

Ø         用户可以通过左边的textArea得到当前群中用户所发送的消息的记录 [11]

Ø         当用户连接失败,可以选择重新登陆,重新登陆就不需要重新输入用户名。

Ø         假如用户登陆时,没有指定连接地址,将默认为localhost地址 [12]

Ø         用户可以通过直接按Enter键发送消息 [13]

总结
经过一个星期的编码,基本完成了课题任务。从中也学到了不少的东西,锻炼了自己的独立开发能力。其中,对 Java 语言也有了一定的了解,也被 Java 语言的强大类库所折服,以及 Java 环境提供的规范语言所欣喜。正因为有这样优秀的语言,和优秀的类库使得这次的任务能顺利的完成。
从中让我深有体会的是, Java 的多线程编程。让我真正有机会接触多线程的编程,而 Java 语言的强大也使得这样的一个过程,不是非常的艰难。 Java 多线程编程,一般采用继承 Thread 类或者采用 Runnable 接口来实现。
Windows 平台和 Linux 平台对于 Java 语言,不同的只是虚拟机,对于程序,对于编码没有区别,这也是能让我顺利完成 Linux 平台的应用程序的一个保证。
通过书写这篇文档,我也从中琢磨了许多的东西,如 Rose UML 等面向对象实现概念,通过尝试也学习了其中工具带来的方便。
 
参考资料
以下是开发过程中参考过的资料,其中有网页模式,其中有课本,以及有用的信息。
 

m        Ineroduction to Java Programming Third Editon :     Y.Daniel Liang

 

m        Linux Platform - J2SE(TM) and NetBeans(TM) IDE : http://java.sun.com

 

m        Java sockets 101:             http://www.ibm.com/developerWorks

 

m        Building a Java chat server:     http://www.ibm.com/developerWorks

 

m        Beej网络socket编程指南:    http://www.ecst.csuchico.edu/%7Ebeej/guide/net/

 

m        UML参考手册 :             James Rumbaugh

 
 
 
 


[1] 如果试图在被占用的端口上创建一个服务器套接字,将引起java.net.BindException 实时错误

[2] 该方法可以引起死锁。
[3] 唤醒线程一般不使用该方法,而是采用notify()方法加布尔变量来指明线程是否唤醒。

[4] Website: https://jsecom15d.sun.com/ECom/EComActionServlet;jsessionid=6596D10F28E751B8FD7981BCCB5E02DA#http://192.18.97.252/ECom/EComTicketServlet/BEGIN6596D10F28E751B8FD7981BCCB5E02DA/-2147483648/957453423/1/626894/626858/957453423/2ts+/westCoastFSEND/jdk-1.5.0_04-nb-4.1-oth-JPR/jdk-1.5.0_04-nb-4.1-oth-JPR:2/jdk-1_5_0_04-nb-4_1-linux.bin

[5] 图中所示的界面是Windows平台的界面效果,Linux(Rad hat Linux 9.0)下的执行界面的布局方式是大致相同的。

[6] 这里有个约定,就是用户名应该是唯一的。
[7] 其实是Java的向量类(Vector),可以动态的调整大小,使用Java提供的函数可以很好实现数据的输入保存和输出得到

[8] 在实现用,我并没有这样做,而是将其赋离线常量(String), 这样的目的,在后面将叙述到

[9] 这样做的目的,是为了处理简单,当然在某些时候也是需要这样处理的
[10] 可以实现多人私聊,可以将你的信息,发给你要想让看到的人。而不用发给全部
[11] 若功能再扩展,可以实现将聊天的历史记录实时保存
[12] 本地的调试地址
[13] 这是通过调用textArea的键盘按键事件来实现的
  • 0
    点赞
  • 3
    收藏
    觉得还不错? 一键收藏
  • 1
    评论

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论 1
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值