网络聊天室(Java)

来源:互联网 发布:阿里云的dns是多少 编辑:程序博客网 时间:2024/05/21 09:35
摘要

本文阐述了基于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 客户端和服务器端的基本流程
 
流程图的简要描述
ServerClient端通信主要是服务器端创建多个线程,生成多个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平台的应用程序的一个保证。
通过书写这篇文档,我也从中琢磨了许多的东西,如RoseUML等面向对象实现概念,通过尝试也学习了其中工具带来的方便。
 
参考资料
以下是开发过程中参考过的资料,其中有网页模式,其中有课本,以及有用的信息。
 

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的键盘按键事件来实现的
原创粉丝点击