本文阐述了基于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:
图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采用J2SE和NetBeans可以很容易的开发面向Linux的应用程序,可以移植到windows平台下运行。其中NetBean 是Sun公司开发的免费Java图形用户界面编辑器。(如图1-4)可以很轻松的实现界面的设计,它将控件以Swing, awt, JavaBean分类放置。
集成
Tomcat 5.0
加入对
XML,Structs
的支持。这其中感触最深的地方是,不能修改自动生成的组件初始代码。假如要用
ButtonGroup
就得自己去另写一个函数来初始化。感觉这样做,因为不能随意修改代码,能避免随意修改所导致的错误。但是,很多时候,我们真的要修改那部分代码反倒是件很麻烦的事了。
对于
window
平台有个比较不当的地方就是内存消耗太大。硬盘频繁访问。在
Linux
环境可以明显感觉到速度快了不少!
图1-4 windows 平台下的NetBeans运行界面 [5]
服务器端和客户端体系结构
根据通信的基本原理,不难分析服务器端与客户端的通信实现,以下是客户端和服务器端的交互流程,如图
1-5
图1-5 客户端和服务器端的基本流程
流程图的简要描述
Server
和
Client
端通信主要是服务器端创建多个线程,生成多个
Socket
对不同的用户进行通信。服务器端和客户端通过消息命令字的方式进行消息确认。方式是在消息头加入“命令字”。
自定义命令字含义如下:
[MESSAGE]
:表示接下来的一句话是消息
|
|
[NAME]: 表示接下来的一句话是名字
|
[FIRSTNAME]
:用于程序逻辑控制,表示第一个
|
[SYSTEM]: 系统消息
|
[Server exit!]
:
服务器退出
|
[WHISPERMESSAGE]
:私聊控制字
|
|
[QUIT]
:
表示客户端退出聊天室
|
|
客户端:
客户端由两个类实现,一个是主类
ClientJFrame
另外一个是用于播放声音的
PlaySound
类。一下是客户端的类关系图,图
1-6
:
图1-6 Client端两个类,ClientJFrame中创建PlaySound实例来播放声音
Ø
图1-7 Server端的类视图
Ø Server相对与客户端更加的复杂,要主动监听客户端发送的连接请求,创建不同的线程,来应答客户的请求。创建的线程,接受客户发送的数据的处理。
Server创建了连接线程后,还必须创建广播线程,将每一个客户发送的消息广播出去,到每一个客户端。对于广播线程和数据接受和处理线程之间的资源共享问题,我采用了,有序运行的方法来消除死锁。即在用户发送来数据时,开启广播线程,对刚才的数据进行广播,在信息广播结束后,关闭广播线程。这样一前一后,就可以保证数据广播的正确性。
在server还需要处理的一件事,就是如何将私聊信息发送给指定的客户端。我采用的方式是,“用户名查找发送法”,可以比较快而准确的发送数据 [6] ,为了和群聊消息区分,我采用WhisperThread类单独给予处理。这样可以清晰的区分。
在客户端,还需要处理的一件事情就是,管理当前的在线用户。我使用一个堆栈 [7] 来管理用户,当用户来到时,就将用户名压入堆栈。当用户退出时,将用户的名字从中去除 [8] 。
Ø 服务端使用serverListen()函数开始监听端口,其函数原型如下:
图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 实时错误
[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)下的执行界面的布局方式是大致相同的。
[7] 其实是Java的向量类(Vector),可以动态的调整大小,使用Java提供的函数可以很好实现数据的输入保存和输出得到
[8] 在实现用,我并没有这样做,而是将其赋离线常量(String), 这样的目的,在后面将叙述到
[9] 这样做的目的,是为了处理简单,当然在某些时候也是需要这样处理的
[10] 可以实现多人私聊,可以将你的信息,发给你要想让看到的人。而不用发给全部
[11] 若功能再扩展,可以实现将聊天的历史记录实时保存
[13] 这是通过调用textArea的键盘按键事件来实现的