实训报告
1 实训目的
以Java程序设计语言为基础,以Eclipse为开发平台,通过完成简易聊天程序的编写,调试,运行工作,进一步掌握Java应用程序的开发方法和编程技巧,巩固理论知识。
2 实训内容
系统的模块划分和各模块的功能介绍:
简易的多人聊天软件,支持任意两个客户端的私聊,所有客户之间的群聊,只需启动一个服务器端程序,允许多人上线聊天。该软件分为客户端(client)和服务端(server)两个模块,在一台计算机上启动服务器端程序,客户端使用用户名(昵称)登录,服务器端验证此昵称当前未使用准予登陆,否则其拒绝上线。用户上线后,在聊天界面,右下的下拉框选择聊天对象,可以是任意在线用户,也可以是所有人选择聊天。同时用户在服务器端的界面可以查看用户的上线、下线情况,所有用户的聊天记录。
3 详细设计与实现
3.1 服务器启动的实现
运行IMServerFrame类,出现服务器端启动界面窗口,如下图:
服务器启动界面截图
IMPanelClient类(JFrame)运行后,服务器启动,创建空的在线用户列表,创建服务器端ServerSocket,绑定6666端口,启动监听线ServerListenThread类,如果有用户发来连接请求立刻接受,判断接收客户传来的消息是不是“login”。如果是“login”,表示该用户想登陆,接续接收用户传来的消息,即 “用户名”。此时服务器端启动JDBCThread类查询当前用户列表,验证用户名是否已被使用。
1.为IMServerFrame类添加函数startServer(),如下:
public void startServer() throws IOException{
ServerSocket server=new ServerSocket(6666,100);
chatMeg.append("服务器程序已经启动…………"+'\n');
startServer.setEnabled(false);
stopServer.setEnabled(true);
userInfoList =new Vector();
userNameList=new Vector();
ServerListenThread listenThread=newServerListenThread(server,chatMeg,userInfoList,userNameList,userOnlineList);
listenThread.start();
}
2.为“启动服务”按钮添加点击事件,调用startServer()函数:
startServer.addActionListener(new ActionListener() {
public void actionPerformed(ActionEvent e) {
try {
startServer();
} catch (Exception e1) {
// TODO Auto-generated catch block
e1.printStackTrace();
}
}
});
panel.add(startServer);
startServer.setText("发送")
3.为ServerListenThread类添加函数startListen:
public void startListen(){
while(!isStop&&!server.isClosed()){
try{
SingleUserMegData clientData=new SingleUserMegData();
Socket socket=server.accept();
ObjectOutputStream out=new ObjectOutputStream(socket.getOutputStream());
out.flush();
ObjectInputStream in =new ObjectInputStream(socket.getInputStream());
clientData.setSocket(socket);
clientData.setDataOut(out);
clientData.setDataIn(in);
String msg=(String)in.readObject();
if(msg.equals("Login")){
String checkUserName=(String)in.readObject();
JDBCThread jt=new JDBCThread(chatMeg,userInfoList,userNameList,
userOnlineList,clientData,checkUserName);
jt.start();
}
}
catch(Exception e){
JOptionPane.showMessageDialog(null,"您已经关闭了侦听套接字!!!",
"系统警告",JOptionPane.ERROR_MESSAGE);
}
}
}
为ServerListenThread类添加run函数如下,调用startListen方法,开始监听:
public void run(){
if(!server.isClosed()){
startListen();
}
}
}
3.2 用户远程登录的实现
用户a登陆时,向服务器发送“login” ,服务器端通过ServerListenThread类,判断接收客户传来的消息是不是“login”,如果是“login”,表示该用户想登陆,接续接收用户传来的消息,即 “用户名”。此时服务器端启动JDBCThread类查询当前用户列表,如果a已经在列表中,向客户端发送消息“fail”则不允许a登陆,如果a不在列表中,向该客户端发送消息”success”,允许登陆,并把a加入当前用户列表。
在client包下创建登陆窗口类UserLogin,登陆界面如图所示:
登陆的界面截图
1.在UserLogin的“登陆“按钮事件中添加代码
butLogin.addActionListener(new ActionListener() {
public void actionPerformed(ActionEvent arg0) {
String userName=userNameField.getText();
if(userName==null||userName.equals("")){
JOptionPane.showMessageDialog(null,"昵称不能为空",
"错误提示",JOptionPane.ERROR_MESSAGE);
return;
}
try{
Socket clientSocket=new Socket("127.0.0.1",5549);
ObjectInputStream dataIn =new ObjectInputStream(clientSocket.getInputStream());
ObjectOutputStream dataOut =new ObjectOutputStream(clientSocket.getOutputStream());
boolean checkID=isLogin(dataIn,dataOut,userName);
if(checkID){
dispose();
IMPanelClient client=new IMPanelClient(clientSocket, dataIn,dataOut,userName);
client.setVisible(true);
} else{
JOptionPane.showMessageDialog(null,"此昵称以被使用,请换一个!","错误提示",JOptionPane.ERROR_MESSAGE);
}
} catch(Exception e){
JOptionPane.showMessageDialog(null,"服务器端没有响应",
"错误提示",JOptionPane.ERROR_MESSAGE);
}
}
});
2.为UserLogin类添加isLogin方法:
public boolean isLogin(ObjectInputStream dataIn,
ObjectOutputStream dataOut,
String userName){
boolean checkResult=false;
try{
dataOut.writeObject("Login");
dataOut.flush();
dataOut.writeObject(userName);
dataOut.flush();
String check=(String)dataIn.readObject();
if(check.equalsIgnoreCase("success")){
checkResult=true;
} else
checkResult=false;
} catch(Exception e){
e.printStackTrace();
}
return checkResult;
}
3.为JDBCThread类添加check方法,验证用户名和密码:
private void check(){
try{
if(!userNameList.contains(checkUserName)){
clientData.getDataOut().writeObject("success");
clientData.getDataOut().flush();
clientData.setUserName(checkUserName);
chatMeg.append(checkUserName+"上线了!"+"\n");
userInfoList.add(checkUserName);
userOnlineList.addItem(clientData);
else{
clientData.getDataOut().writeObject("fail");
clientData.getDataOut().flush();
}
} catch(Exception e){
e.printStackTrace();
}
}
3.3 用户下线的实现
JDBCThread类在向用户发送允许登陆的success消息后,启动ServerReceiveThread线程,该线程用于服务器端与某一客户端收发消息(包括聊天消息,下线消息)。该线程通过megManage()方法判断,如果收到的消息表示当前是用户下线消息,从在线用户列表中删除此用户,反之收到的消息表示当前是用户发来的聊天信息,将此聊天信息转发给指定人。
public void megManage(){
while(!isStop&&clientData.getSocket().isClosed()){
try{
String messageType=(String)clientData.getDataIn().readObject();
if(messageType.equalsIgnoreCase("用户下线")){
String userName =(String)clientData.getUserName();
chatMeg.append((userName+"下线了"+"\n"));
userInfoList.remove(clientData);
userNameList.remove(userName);
userOnlineList.removeItem(clientData);
break;
}
} catch(Exception e){
e.printStackTrace();
System.exit(0);
}
}
}
3.4 用户列表的发送
用户a登陆时,服务器端通过JDBCThread类查询当前用户列表,如果a已经在列表中,向客户端发送消息“fail”则不允许a登陆,如果a不在列表中,向该客户端发送消息”success”,允许登陆,并把a加入当前用户列表。
有用户登录时,向所有用户发送当前的在线用户列表,在JDBCThread类的check()函数中if (!userNameList.contains(checkUserName))中补充代码,如下:
String[] userArray=(String[])userNameList.toArray(new String[0]);
for(int i=0;i<userInfoList.size();i++){
SingleUserMegData clientData =(SingleUserMegData)userInfoList.get(i);
clientData.getDataOut().writeObject("用户列表");
clientData.getDataOut().flush();
clientData.getDataOut().writeObject(userArray);
clientData.getDataOut().flush();
}
用户下线时,在ServerReceiveThread类的megManage()方法中,向目前所有在线用户发送一份在线用户列表。
为IMPanelClient类添加窗口关闭事件,保证窗口关闭时,向服务器发送“用户下线“消息,并关闭和服务器端的连接:
addWindowListener(new WindowAdapter(){
public void windowClosing(WindowEvent e){
if(clientSocket!=null&&!clientSocket.isClosed()){
try{
dataOut.writeObject("用户下线");
dataOut.flush();
Thread.sleep(1000);
clientSocket.close();
} catch (Exception ex){
ex.printStackTrace();
}
}
}});
3.5 聊天的实现
当客户端的“发送”按钮或者文本框的回车事件中,向服务器端发送”聊天信息”+对方用户名+信息正文
服务器ServerReceiveThread类的 megManage()方法中添加一个判断,如果收到的消息以“聊天信息”开头,即认为是聊天信息,按照“对方用户名”找到相应的用户,将信息转发给他,如果“对方用户名”为“所有人”,则把这条消息转发给所有在线用户。
客户端ClientReceiveThread类的recevThread(),增加判断,如果收到的消息以“聊天信息”开头,则把此条信息显示在客户端界面上。
聊天消息格式:
私聊时,客户端发送:“聊天信息”+对方用户名+信息正文
群聊时,客户端发送:“聊天信息”+“所有人”+信息正文
服务端收到后转发:
私聊,转发给指定人:“聊天信息”+ 发送方用户名 + 时间 + 信息正文
群聊,转发给在线的所有人:“聊天信息”+发送方用户名+“群发” + 时间+信息正文。 聊天窗口如下:
聊天窗口的截图
1.客户端发送聊天信息的实现
为IMPanelClient类的“发送“按钮事件(或者文本框回车事件)添加下面代码:
ublic void actionPerformed(ActionEvent arg0) {
if(clientSocket!=null&&!clientSocket.isClosed()){
try{
String message=textField.getText();
String toSomebody=(String)onlineBox.getSelectedItem();
dataOut.writeObject("聊天信息");
dataOut.flush();
dataOut.writeObject(toSomebody);
dataOut.flush();
dataOut.writeObject(message);
dataOut.flush();
java.util.Date d=new java.util.Date();
SimpleDateFormat s=new SimpleDateFormat("yyyy-MM-dd HH:mm:ss");
String dateStr=s.format(d);
receiveMeg.append(userName+" "+dateStr+"\n"+message+"\n");
textField.setText("");
} catch(Exception e){
}
}
}
});
2.服务器端接收聊天信息的实现
为ServerReceiveThread类的megManage方法添加下面代码(写在用户下线的if块结束后):
else if(messageType.equalsIgnoreCase("聊天信息")){
String toSomebody=(String)clientData.getDataIn().readObject();
String message=(String)clientData.getDataIn().readObject();
java.util.Date d=new java.util.Date();
SimpleDateFormat s=new SimpleDateFormat("yyyy-MM-dd HH:mm:ss");
String dateStr=s.format(d);
chatMeg.append(dateStr+"\n");
chatMeg.append(clientData.getUserName()+"对"+toSomebody+"说:"+message+"\n");
if(toSomebody.equalsIgnoreCase("所有人")){
for(int i=0;i<userInfoList.size();i++){
SingleUserMegData toSomebodyData=(SingleUserMegData)userInfoList.get(i); if(toSomebodyData.getUserName().equals(clientData.getUserName())){
continue;
} if(toSomebodyData.getSocket()!=null&&!toSomebodyData.getSocket().isClosed()){
toSomebodyData.getDataOut().writeObject("聊天信息");
toSomebodyData.getDataOut().flush(); toSomebodyData.getDataOut().writeObject(clientData.getUserName()+"群发:"+dateStr+"\n"+message+"\n");
toSomebodyData.getDataOut().flush();
}
}
} else{
int index=userNameList.indexOf(toSomebody);
SingleUserMegDatatoSomebodyData=(SingleUserMegData)userInfoList.get(index);
if(toSomebodyData.getSocket()!=null
&&toSomebodyData.getSocket().isClosed()){
toSomebodyData.getDataOut().writeObject("聊天信息");
toSomebodyData.getDataOut().flush();
toSomebodyData.getDataOut().writeObject(
clientData.getUserName()+" "+dateStr+
"\n"+message+"\n");
toSomebodyData.getDataOut().flush();
}
}
}
3.客户端接收聊天信息的实现
为ClientReceiveThread类的recevThread()方法添加判断,如果收到消息以“聊天信息开头“,把此信息显示在客户端窗口上:
public void recevThread(){
while(!isStop&&clientSocket!=null&&clientSocket.isClosed()){
try{
String megType=(String)dataIn.readObject();
if(megType.equalsIgnoreCase("用户列表")){
String[] userList=(String[])dataIn.readObject();
userOnlineList.removeAllItems();
userOnlineList.addItem("所有人");
for(int i=0;i<userList.length;i++){
userOnlineList.addItem(userList[i]);
}
} else if(megType.equalsIgnoreCase("聊天信息")){
String message=(String)dataIn.readObject();
receiveMeg.append(message); receiveMeg.setCaretPosition(receiveMeg.getDocument().getLength());
}
}catch(Exception e){
System.exit(0);
}
}
}
4 总结和心得
在开学的第一周,我们进行了为期一周的JAVA实训。现在结束了,回首上学期的java学习,重点还是在学习概念等一些常识性的东西,知道了JAVA语言的起源和发展,以及JAVA的特点,类及其方法,对象,数据类型,变量,包和接口,继承和多态,输入输出流,学会分析异常,抛出异常,后期主要是小程序运用,窗口界面设计和事件。我觉得这两种程序结构有很大的不同,不管是体系结构还是运行方式,都有很大的区别,我主要偏向于小程序的学习,因为感觉它用处比较大,可以做出好多好多好玩的游戏,运用程序等,且它灵活。呵呵,当然学知识可不能凭自己的爱好和一时兴趣,要一步一个脚印,认认真真,踏踏实实,理论与实践相结合,在扎实掌握课本实例和内容之后,有一定的扩展阅读和课外学习,充分全面的了解JAVA的应用和扩展运用。
在我所学的语言当中,我自认为JAVA是一门比较强大的面向对象的编程语言,不仅仅因为它的跨平台性,更多的是因为它的灵活多变和实用性较强,可以说比较的经典和强悍。
所以学好java语言有很大的用处,这次实训,面对一道陌生的代码和错误时,不应慌张和无措,首先应该想到这和课本上的哪些知识具有结合点,回忆和分析这种结构的算法和具体实施方法,综合考虑其他的方面。在编写时,我总是粗心大意把字母打错,不区分大小写,在报错后煞费苦心的去寻找那个小小的错瓦,所以,在编写JAVA代码时应该小心谨慎去输入哪怕是一个小小的符号,一个常量变量的设定,一个字母的大小写,这都无不考量着我们的细心和严谨,所以学习JAVA,不仅对我们以后学习其他语言有很大的好处,而且也让我们知道了和理解了作为一个编程人员首先应具有的良好心理素质,那就是冷静思考和专心致志。对待学术知识应该是严谨和认真。
这次实训,我们更多学到的是不懂就问和自己应该尽自己的全力去尝试,哪怕失败,只要自己尽自己的全力,和身边同学一起探讨而不是抄袭,团结合作,发挥团队意识,最后在自己的努力下,终于运行成功,这种成就感美不可言,心情愉悦至极。
最后终于要结束了,大家都有种释怀的感觉,当然我想我更多的是兴奋和自己掌握了知识之后的饱满感,学知识就像吃东西一样,吃饱了就应该好好的消化。要不断的温习和运用,做到举一反三,将所学知识充分融入到平时的学习生活中去,为以后的工作做好坚实的基础。感谢学校和老师能够为我们提供这次机会,让我们更好的掌握和了解JAVA这门语言。谢谢!