网络聊天室(Java)
本文阐述了基于Linux环境,Java语言实现的基本聊天室功能,涉及Linux下的Java 语言的Socket编程。以及Java语言的多线程编程。
<!--[if !supportLists]-->Ø <!--[endif]-->TCP(Transmission Control Protocol)基础
<!--[if !supportLists]-->Ø <!--[endif]-->UDP(User Datagram Protocol)基础
<!--[if !supportLists]-->Ø <!--[endif]-->Socket(Java)
Java支持流套接字(steam socket)和数据报套接字(datagram socket)。流套接字使用TCP协议(Transmission Control Protocol, 传输控制协议)进行数据的传输,而数据报套接字使用UDP协议(User Datagram Protocol, 用户数据报协议)。因为TCP能够探测丢失的数据传输并重新提交它们,因此传输的数据不会丢失,是可靠的。相比之下,UDP协议不能保证无损失传输。所以,采用TCP协议通信可以保证数据的正确传输。
<!--[if !supportLists]-->Ø <!--[endif]-->客户/服务器模式
<!--[if gte vml 1]> <![endif]--><!--[if !vml]--><!--[endif]-->
<!--[if gte vml 1]> <table align="left" cellpadding="0" cellspacing="0"> <tbody><tr> <td height="1" width="55"><br></td> </tr> <tr> <td><br></td> <td style="border: 0.75pt solid black; background: white none repeat scroll 0%; vertical-align: top; -moz-background-clip: -moz-initial; -moz-background-origin: -moz-initial; -moz-background-inline-policy: -moz-initial;" bgcolor="white" height="38" width="550"><!--[endif]--><!--[if !mso]-->
<!--[endif]-->
ServerSocket server = new ServerSocket(port);1 |
<!--[if gte vml 1]> <table align="left" cellpadding="0" cellspacing="0"> <tbody><tr> <td height="3" width="55"><br></td> </tr> <tr> <td><br></td> <td style="border: 0.75pt solid black; background: white none repeat scroll 0%; vertical-align: top; -moz-background-clip: -moz-initial; -moz-background-origin: -moz-initial; -moz-background-inline-policy: -moz-initial;" bgcolor="white" height="38" width="550"><!--[endif]--><!--[if !mso]-->
<!--[endif]-->
Socket connectToClient = server.accept(); |
<!--[if gte vml 1]> <table align="left" cellpadding="0" cellspacing="0"> <tbody><tr> <td height="6" width="55"><br></td> </tr> <tr> <td><br></td> <td style="border: 0.75pt solid black; background: white none repeat scroll 0%; vertical-align: top; -moz-background-clip: -moz-initial; -moz-background-origin: -moz-initial; -moz-background-inline-policy: -moz-initial;" bgcolor="white" height="39" width="550"><!--[endif]--><!--[if !mso]-->
<!--[endif]-->
Socket connectToServer = new Socket(ServerName, port); |
<!--[if gte vml 1]> <table cellpadding="0" cellspacing="0"> <tbody><tr> <td style="border: 0.75pt solid black; background: white none repeat scroll 0%; vertical-align: top; -moz-background-clip: -moz-initial; -moz-background-origin: -moz-initial; -moz-background-inline-policy: -moz-initial;" bgcolor="white" height="60" width="550"><!--[endif]--><!--[if !mso]-->
<!--[endif]-->
InputStream isFromServer = connectToServer.getInputStream();
OutputStream osToServer= connectToServer.getOutputSteam();
|
InputStream 和 OutputSteam是用户读写字节。可以使用DataInputStream、DataOutputSteam、BufferReader 和 PrintWriter包装InputStream和OutputSteam,读取double、int、String之类的数据值。可以使用readLine方法读入一行数据,使用println向端口写入一行。
<!--[if gte vml 1]> <tr> <td><![endif]> <div> <p align=center style='text-align:center'><span style='mso-ascii-"Times New Roman"; mso-hansi-"Times New Roman"'>任务</span>1</p> </div> <![if !mso]></td> </tr> </table> <![endif]> <tr> <td><![endif]> <div> <p align=center style='text-align:center'><span style='mso-ascii-"Times New Roman"; mso-hansi-"Times New Roman"'>任务</span>2</p> <div> </div> </div> <![if !mso]></td> </tr> </table> <![endif]> <tr> <td><![endif]> <div> <p align=center style='text-align:center'><span style='mso-ascii-"Times New Roman"; mso-hansi-"Times New Roman"'>任务</span>3</p> <div> </div> </div> <![if !mso]></td> </tr> </table> <![endif]> <![endif]--><!--[if !vml]-->
<!--[if !supportLists]-->Ø <!--[endif]-->线程有五种状态:新建、就绪、运行、阻塞、结束。
<!--[if gte vml 1]> <![endif]--><!--[if !vml]--><!--[endif]-->
<!--[if !supportLists]-->Ø <!--[endif]-->Thread类包括以下的几种控制方法:
public static void sleep(long millis) throws InterruptedException 方法,可以将在运行的线程置为休眠状态,休眠时间为指定的毫秒。
<!--[if !supportLists]-->¨ <!--[endif]-->Linux Platform - J2SE(TM) and NetBeans(TM) IDE Bundle NB 4.1 / J2SE 5.0 Update 4<!--[if !supportFootnotes]-->[4]<!--[endif]-->
<!--[if !supportLists]-->¨ <!--[endif]-->Rat Hat Linux 9.0
在Linux采用J2SE和NetBeans可以很容易的开发面向Linux的应用程序,可以移植到windows平台下运行。其中NetBean 是Sun公司开发的免费Java图形用户界面编辑器。(如图1-4)可以很轻松的实现界面的设计,它将控件以Swing, awt, JavaBean分类放置。
<!--[if gte vml 1]> <![endif]--><!--[if !vml]--><!--[endif]-->
<!--[if gte vml 1]> <![endif]--><!--[if !vml]--><!--[endif]-->
[MESSAGE]
:表示接下来的一句话是消息
| |
[NAME]: 表示接下来的一句话是名字
|
[FIRSTNAME]
:用于程序逻辑控制,表示第一个
|
[SYSTEM]: 系统消息
|
[Server exit!]
:
服务器退出
|
[WHISPERMESSAGE]
:私聊控制字
| |
[QUIT]
:
表示客户端退出聊天室
|
<!--[if gte vml 1]> <![endif]--><!--[if !vml]--><!--[endif]-->
<!--[if !supportLists]-->Ø <!--[endif]--><!--[if gte vml 1]> <table cellpadding="0" cellspacing="0"> <tbody><tr> <td style="border: 0.75pt solid black; background: white none repeat scroll 0%; vertical-align: top; -moz-background-clip: -moz-initial; -moz-background-origin: -moz-initial; -moz-background-inline-policy: -moz-initial;" bgcolor="white" height="404" width="582"><!--[endif]--><!--[if !mso]-->
<!--[endif]-->
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();
}
|
<!--[if !supportLists]-->Ø <!--[endif]-->通过SendInformation()方法实现数据的发送,实现函数原型如下:
<!--[if gte vml 1]> <table align="left" cellpadding="0" cellspacing="0"> <tbody><tr> <td height="3" width="23"><br></td> </tr> <tr> <td><br></td> <td style="border: 0.75pt solid black; background: white none repeat scroll 0%; vertical-align: top; -moz-background-clip: -moz-initial; -moz-background-origin: -moz-initial; -moz-background-inline-policy: -moz-initial;" bgcolor="white" height="259" width="582"><!--[endif]--><!--[if !mso]-->
<!--[endif]-->
public void sendInformation()
{
if(私聊)
{
osToServer.println("[WHISPERMESSAGE]" + message);//发送私聊信息
osToServer.flush();
}
else
{
osToServer.println("[MESSAGE]" + message); //发送群聊信息 osToServer.flush();
}
}
|
<!--[if !supportLists]-->Ø <!--[endif]-->当程序退出,或者服务器退出,线程应该结束运行。客户端,通过重载Thread的run方法实现,函数的原型如下:
<!--[if gte vml 1]> <table align="left" cellpadding="0" cellspacing="0"> <tbody><tr> <td height="2" width="23"><br></td> </tr> <tr> <td><br></td> <td style="border: 0.75pt solid black; background: white none repeat scroll 0%; vertical-align: top; -moz-background-clip: -moz-initial; -moz-background-origin: -moz-initial; -moz-background-inline-policy: -moz-initial;" bgcolor="white" height="397" width="582"><!--[endif]--><!--[if !mso]-->
<!--[endif]-->
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);
}
}
|
<!--[if !supportLists]-->à <!--[endif]-->在退出应用程序之前,向服务器端发送[QUIT]命令字,实现聊天室的更新。
<!--[if !supportLists]-->à <!--[endif]-->客户端退出要将socket关闭,要将输入和输出的数据流关闭。
<!--[if !supportLists]-->à <!--[endif]-->最后将执行finally块程序,最后调用System的exit的函数退出应用程序。
<!--[if gte vml 1]> <![endif]--><!--[if !vml]--><!--[endif]-->
<!--[if !supportLists]-->Ø <!--[endif]-->Server相对与客户端更加的复杂,要主动监听客户端发送的连接请求,创建不同的线程,来应答客户的请求。创建的线程,接受客户发送的数据的处理。
<!--[if !supportLists]-->Ø <!--[endif]-->服务端使用serverListen()函数开始监听端口,其函数原型如下:
<!--[if gte vml 1]> <table align="left" cellpadding="0" cellspacing="0"> <tbody><tr> <td height="13" width="31"><br></td> </tr> <tr> <td><br></td> <td style="border: 0.75pt solid black; background: white none repeat scroll 0%; vertical-align: top; -moz-background-clip: -moz-initial; -moz-background-origin: -moz-initial; -moz-background-inline-policy: -moz-initial;" bgcolor="white" height="169" width="582"><!--[endif]--><!--[if !mso]-->
<!--[endif]-->
private void serverListen()
{
chatAcceptThread = new Thread(this); //创建一个监听线程 chatAcceptThread.start(); broadcastThread = new BroadcastThread(this); //创建广播线程 broadcastThread.start();
}
|
<!--[if !supportLists]-->Ø <!--[endif]-->ServerJFrame是通过Runnable接口来创建的线程。其run函数实现客户端的接受并创建一个新的线程:
<!--[if gte vml 1]> <table align="left" cellpadding="0" cellspacing="0"> <tbody><tr> <td height="5" width="31"><br></td> </tr> <tr> <td><br></td> <td style="border: 0.75pt solid black; background: white none repeat scroll 0%; vertical-align: top; -moz-background-clip: -moz-initial; -moz-background-origin: -moz-initial; -moz-background-inline-policy: -moz-initial;" bgcolor="white" height="398" width="582"><!--[endif]--><!--[if !mso]-->
<!--[endif]-->
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){}
}
|
为了后面能够将数据广播出去,和实现私聊,必须要得到响应的线程,所以在向堆栈压入线程的时候,需要有一个变量(index)来指示线程, index 不会随着客户的退出而删除<!--[if !supportFootnotes]-->[9]<!--[endif]-->,而是逐次累加,那么当客户退出时,要将此进程在堆栈中的位置设置为[EMPTY],来表示一个客户端已经退出,此时,服务器端要结束和这个客户端连接的线程。
<!--[if !supportLists]-->Ø <!--[endif]-->那么当用户发来数据的时候,并且命令字为[MESSAGE]的时候,服务器需要将这条信息广播出去,这个由Broadcast来处理,其中的run函数原型如下:
<!--[if gte vml 1]> <table align="left" cellpadding="0" cellspacing="0"> <tbody><tr> <td height="6" width="31"><br></td> </tr> <tr> <td><br></td> <td style="border: 0.75pt solid black; background: white none repeat scroll 0%; vertical-align: top; -moz-background-clip: -moz-initial; -moz-background-origin: -moz-initial; -moz-background-inline-policy: -moz-initial;" bgcolor="white" height="789" width="582"><!--[endif]--><!--[if !mso]-->
<!--[endif]-->
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){}
}
|
<!--[if gte vml 1]> <![endif]--><!--[if !vml]--><!--[endif]-->
<!--[if !supportLists]-->Ø <!--[endif]-->程序可以实现公聊和私聊<!--[if !supportFootnotes]-->[10]<!--[endif]-->,公聊在服务器端将加入聊天记录,私聊则只是发给指定用户,服务器端不保留聊天信息。
<!--[if !supportLists]-->Ø <!--[endif]-->收到系统消息,和用户变化都会有声音提示。
<!--[if !supportLists]-->Ø <!--[endif]-->完全可以单机来调试信息,也试过在Linux下运行服务器端,在Windows下使用客户端进行访问,访问方式没有区别,通信也没有故障。
<!--[if !supportLists]-->Ø <!--[endif]-->当服务器退出时,或者说用户端失去服务器连接时,用户将需要重新连接,当然也可以实现超时退出的方式,这样可以实现重新连接。
<!--[if !supportLists]-->Ø <!--[endif]-->可扩展功能:系统可以选择需要发送的系统消息的对象,这样可以使系统消息发送更加灵活。
<!--[if !supportLists]-->Ø <!--[endif]-->用户可以通过右边的list得到当前的在线用户的状况
<!--[if !supportLists]-->Ø <!--[endif]-->用户可以通过左边的textArea得到当前群中用户所发送的消息的记录<!--[if !supportFootnotes]-->[11]<!--[endif]-->。
<!--[if !supportLists]-->Ø <!--[endif]-->当用户连接失败,可以选择重新登陆,重新登陆就不需要重新输入用户名。
<!--[if !supportLists]-->Ø <!--[endif]-->假如用户登陆时,没有指定连接地址,将默认为localhost地址<!--[if !supportFootnotes]-->[12]<!--[endif]-->。
<!--[if !supportLists]-->Ø <!--[endif]-->用户可以通过直接按Enter键发送消息<!--[if !supportFootnotes]-->[13]<!--[endif]-->。