这次聊天室项目,是进培优班以来第一个小组项目.要求是用swing(没学过,自行百度)写一个聊天室.对我来说也算是对java基础部分的一个总结吧.感觉基本上java基础学到的东西都用上了.
因为swing我也不懂,也没兴趣去搞这个过时的技术.我就完全没有关心了.我主要负责的是聊天室后台服务器方面的事情.接下来总结一下学到的东西.
1. 一开始我是准备使用String来当作客户端和服务器的传输介质的…通过程序来拼接关键字,然后客户端和服务器分别解析String来获取需要的信息…比如说用@符号来表示消息类型,然后用#号来分割发送元素,比如说时间和聊天内容.
但是后来在刘璞和百宝箱的强烈反对下改用了一个chatbean,一个对象.这个对象使用一个int type来区分消息类型,包含整个程序所有需要的消息类型.通过不同的type放入不同的内容,然后通过getSet方法来直接获取.
这个算是学到了,确实比String方式要好很多,但是局限是只有同是java程序才能使用序列化来传输.
chatbean代码如下
package function;
import java.io.Serializable;
import java.util.HashSet;
/**
* 数据传输协议类
* @author 璞
*
*/
public class ChatBean implements Serializable {
//chatBean 版本:1.0
private static final long serialVersionUID = 1L;
/*(1,2,3属于客户端发送给服务器格式)
* type=1 表示 userNmae和passWord(登录时发送)
* type=2 表示 MSGtarget(单人模式和群聊模式切换的时候发)
* type=3 表示 contant(发送聊天内容)
*
* (4,5属于服务器发送给客户端格式)
* type=4 表示 content ,time和fromName(聊天内容,时间,和来自谁)
* type=5 表示 onlineList (在线用户列表,每当有用户上线或下线服务器对所有人广播更新)
* */
/*
* 客户端发送:
* type=1 : username + password (登录界面做)
* type=2 : username + password (注册界面做)
* type=3 : name + time + message (上线,下线,广播 默认设置为@public ;私发为人名)
* type=4 : target 广播@public ;私发为人名
*
*
* 客户端接收:
* type=5 : boolean值,用于判断数据库操作是否成功
* type=6 :name + time + message 接收消息
* type=7 : onlineList 在线用户列表
*/
private int type;
private String userName; //用户名
private String passWord; //密码
private boolean ok; //判断数据库操作是否成功
private String message; //聊天内容
private HashSet onlineList; //在线列表
private String fromName; //发送方昵称(尚未实现)
private String time; //时间
private String MSGtarget; //接收方
public boolean getOk() {
return ok;
}
public void setOk(boolean ok) {
this.ok = ok;
}
2. 然后就是关于程序构架方面,真的很重要…我一开始直接把存储Socket的线程直接存储在集合中.后来发现不行,改了一个需求…就是我们要私聊的时候给不在私聊状态的人发送一个消息,提醒他接收到一条私聊消息…但是我却发现现有构架这个功能加不进去.或者说太费力了…
然后在思考了整整1个小时候推翻了一大半代码全部注释掉,把所有容器的存储对象改为了线程账号唯一对应的ID,一个int值.
然后我发现一个程序写了很久以后,要加功能真的非常麻烦…如果一开始没有预留可拓展空间要加东西真的有种无从下手的感觉哦…以前一直是看论坛上大家说看到垃圾代码烦的要死,重构重构…没有切身体会,这次算是真实体验到了一个程序的可拓展性真的很重要哦.
后期发现BUG,比如说聊天历史记录部分,在一开始的时候只考虑了存储和获取.但是发送消息部分要如何区分不同的历史消息没有一个清晰的思路.在后面写的时候,我好像写了无数个IF,调试了一个晚上才调通…然后,我再也不想碰这段代码…阿西吧….
如果一开始没考虑拓展性后期真的是想死啊….
3. 顺便.写代码的时间和调试bug的时间占比好像是1:3…如果一开始就把框架和各种细节想好,应该只会略微增加写代码的时间,大幅减少调试的时间…
4. 第一次完整的走完一个项目,感觉真的学到了好多…各种细节,switch case,for循环,多线程,生产者消费者模式,集合包装…理解了好多东西,好像把大量的细碎的东西串联在一起了.一下子想不起来太多了,暂时先写这么多吧.
------------------------------------------然后贴一点代码上来吧:千多行有点多,就不全贴了.
1.最重要的线程类
package function;
import java.io.IOException;
import java.io.ObjectInputStream;
import java.io.ObjectOutputStream;
import java.net.Socket;
import java.text.SimpleDateFormat;
import java.util.Date;
import Server_ChatHistory.HistoryLinkedList;
import Server_ChatHistory.HistorySavingThread;
import Server_loginAndRegister.*;
/*
* 客户端对接线程
*/
public class ClientThread extends Thread {
private Socket myself;
private int myId;
private String myName;
private Group MSGgroup;// 该用户所在聊天组
private ObjectOutputStream oos;
private ObjectInputStream ois;
private SimpleDateFormat sdf=new SimpleDateFormat("HH:mm:ss");
ClientThread(Socket myself) {// 构造
this.myself = myself;
try {
oos = new ObjectOutputStream(myself.getOutputStream());
ois = new ObjectInputStream(myself.getInputStream());
} catch (IOException e) {
e.printStackTrace();
System.out.println("客户端线程构造失败!~");
}
}
@Override
public void run() {
System.out.println("新客户端线程启动!~");
try {
while (true) {// -----------------------------------------------线程循环体------------------------------------
ChatBean thisMSG = (ChatBean) ois.readObject();
int type = thisMSG.getType();
switch (type) {// type分支判断!~
case 1:
case1LogIn(thisMSG);
break;
case 2:
case2Registe(thisMSG);
break;
case 3:
case3Chating(thisMSG);
break;
case 4:
case4ChatTypeSwitch(thisMSG);
break;
case 5:
break;
default:
break;
}
}
} catch (Exception e) {
e.printStackTrace();
}
System.out.println("与客户端断开连接或者网络错误");
System.out.println(MainServer.online.geOnlineListByStringSet());
MainServer.online.remove(this);//从在线用户列表中移除自己
System.out.println(MainServer.online.geOnlineListByStringSet());
MSGgroup.remove(this.myId);//从聊天组中移除自己
refreshOnlineList();
try {
oos.close();
ois.close();
} catch (IOException e) {
}
System.out.println(myName+"线程终止运行!~");
}
public void send(ChatBean msg) {//发送消息功能实现
try {
oos.writeObject(msg);
oos.flush();
} catch (IOException e) {
e.printStackTrace();
System.out.println("从"+msg.getUserName()+"发到"+this.getName()+"消息发送失败!~");
}
}
private void case3Chating(ChatBean thisMSG) {
String content=thisMSG.getMessage();
System.out.println(this.myName+"发送了消息:"+content);
// System.out.println(MSGgroup);
String fromName=myName;
String time=sdf.format(new Date());
ChatBean tmp=new ChatBean();
tmp.setType(6);
tmp.setMessage(content);
tmp.setTime(time);
tmp.setUserName(fromName);
MSGgroup.sendToAll(tmp);//发送消息
}
private void case4ChatTypeSwitch(ChatBean thisMSG) {
String target=thisMSG.getMSGtarget();
if(target.endsWith(" (私聊中...)")){
target=target.substring(0,target.indexOf(" "));
}
if(target.equals("@public")) {//切换到公共聊天组
if(MSGgroup.getClass()==PublicChatGroup.class) {
return;//如果处于多人聊天组中,切换到多人聊天将直接返回
}
MainServer.privateGroup.remove((PrivateChatGroup)MSGgroup);
//MSGgroup.remove(this.myId);//切换到公共聊天组//不把自己从私聊组删除
MSGgroup=MainServer.publicGroup;
MSGgroup.add(this.myId);
// System.out.println("接到信号,"+myName+"线程已经切换至公共聊天组");
int t[]= {-1};
HistoryLinkedList linkedList = HistorySavingThread.findList(t);
ChatBean[] arr=linkedList.generateList(20);
for(int i=arr.length-1;i>=0;i--) {
send(arr[i]);
}
refreshOnlineList();
return;//公共聊天组切换逻辑结束
}
//私聊聊天组逻辑:
//检查对方ID是否在私聊组列表里,存在,则加入....不存在,则创建新的私聊聊天组(为了实现离线聊天)
int id=SqlOperation.getIdByUsername(target);//-------------------------登陆模块,拿到账户ID
PrivateChatGroup tmp=MainServer.privateGroup.ifGroupExist(id,this.myId);
MSGgroup.remove(this.myId);//先从公共聊天组中删除自己
if(tmp==null) {
MSGgroup=new PrivateChatGroup();
MSGgroup.add(this.myId);
MSGgroup.add(id);
MainServer.privateGroup.add((PrivateChatGroup)MSGgroup);
}else {
MSGgroup=tmp;
MSGgroup.add(this.myId);
MSGgroup.add(id);
tmp.sendHistory(myId);
}
// System.out.println("接到信号,"+myName+"线程已经切换至私人聊天组");
refreshOnlineList();
}
//-------------------------------------------------------------------------------------------------------------------废弃
// String target=thisMSG.getMSGtarget();
// if(target.equals("public")) {//切换到公共聊天组
// if(MSGgroup.getClass()==PublicChatGroup.class) {
// return;//如果处于多人聊天组中,切换到多人聊天将直接返回
// }
// MSGgroup.remove(this);
// MSGgroup=MainServer.publicGroup;//切换到公共聊天组
// MSGgroup.add(this);
// }
//int id=getIdByUsername(target)//-------------------------登陆模块,拿到账户ID
//ClientThread targetThread=MainServer.online.getThreadByID(id);//通过ID拿到对应线程
// Group tmp=MainServer.privateGroup.findExist(targetThread);//检查对应线程是否已经在私聊组列表里,如果对应线程为空直接返回空
// if(tmp==null) {//如果group不存在,则创建对方为null的group.....这 ..
// MSGgroup=new PrivateChatGroup(this,targetThread);
// MainServer.privateGroup.add((PrivateChatGroup) MSGgroup);//判断双方是否在线在case4Chating()中
// }else {//如果group已经存在,则把自己加入
// MSGgroup=tmp;
// tmp.add(this);
// }
//-------------------------------------------------------------------------------------------------------------------废弃
//
// }
public Group getMSGgroup() {
return MSGgroup;
}
public void setMSGgroup(Group mSGgroup) {
MSGgroup = mSGgroup;
}
private void case2Registe(ChatBean thisMSG) {
System.out.println("进入注册模块");
String username=thisMSG.getUserName();
String password=thisMSG.getPassWord();
ChatBean tmp=new ChatBean();
System.out.println("username="+username+",password="+password);
if(!SqlOperation.isUserExist(username)&&SqlOperation.register(username,password)) { //---------注册模块,注册成功返回true
tmp.setOk(true);//返回客户端,注册是否成功
System.out.println("注册成功");
}else{
tmp.setOk(false);
System.out.println("注册失败");
}
tmp.setType(5);
send(tmp);
}
private void case1LogIn(ChatBean thisMSG) {
String username=thisMSG.getUserName();
String password=thisMSG.getPassWord();
boolean b=SqlOperation.isLoginSuccessful(username,password);//----------------------------------------登陆模块检测方法!
// System.out.println("登陆是否成功"+b);
int id=SqlOperation.getIdByUsername(username);
boolean b2=MainServer.online.ifThisAccountOnline(id);
// System.out.println("是否已经在线"+!b2);
// System.out.println("b:"+b+",b2:"+b2);
if(b&&b2) {
this.myId=id;
MainServer.online.add(this);
MainServer.publicGroup.add(this.myId);
this.myName=username;
this.MSGgroup=MainServer.publicGroup;
// System.out.println("线程已经连入公共聊天组!公共聊天组当前总人数:"+MainServer.publicGroup.size());
}
ChatBean tmp=new ChatBean();
tmp.setType(5);
if(b==true&&b2==true) {
tmp.setOk(true);//返回客户端,允许登陆
System.out.println("服务器允许该线程登陆!");
}else {
tmp.setOk(false);//拒绝登陆
System.out.println("服务器拒绝该线程登陆!");
}
send(tmp);
refreshOnlineList();
}
private void refreshOnlineList() {//更新在线用户列表.
ChatBean tmp=new ChatBean();
tmp.setType(7);
tmp.setOnlineList(MainServer.online.geOnlineListByStringSet());
MainServer.publicGroup.sendToAll(tmp);
MainServer.privateGroup.sendToAll(tmp);
// System.out.println(this.myName+"给全体用户更新在线用户列表!");
}
public int getMyId() {
return myId;
}
public void setMyId(int myId) {
this.myId = myId;
}
public String getMyName() {
return myName;
}
public void setMyName(String myName) {
this.myName = myName;
}
}
2.在线用户列表
package function;
import java.util.ArrayList;
import java.util.HashSet;
//包装过的在线用户列表.
public class OnlineList{
private ArrayList online;
public OnlineList(){
online=new ArrayList<>();
}
synchronized public void add(ClientThread ct) {
// System.out.println(online.size());
online.add(ct);
// System.out.println(online.size());
}
synchronized public void remove(ClientThread ct) {
// System.out.println(online.size());
boolean b=online.remove(ct);
// System.out.println(online.size());
// System.out.println(b);
}
synchronized public int getOnlineNum() {
return online.size();
}
synchronized public boolean ifThisAccountOnline(int id) {//重复登录检查
if(null==getThreadByID(id)) {
// System.out.println("这个ID 不在线返回true");
return true;
}
return false;
}
synchronized public ClinetThread getThreadByID(int id) {//如果在线则返回线程,否则返回null
for (ClientThread t : online) {
if(id==t.getMyId()) {
return (ClinetThread) t;
}
}
return null;
}
synchronized public HashSet geOnlineListByStringSet(){
HashSet tmp=new HashSet<>();
for (ClientThread ct : online) {
String str=ct.getMyName();
if(ct.getMSGgroup().getClass()!=PublicChatGroup.class) {
str=str+" (私聊中...)";
}
tmp.add(str);
}
return tmp;
}
}
3.历史记录处理线程.应用了生产者,消费者模式.这货算消费者.
package Server_ChatHistory;
import java.io.File;
import java.io.FileInputStream;
import java.io.ObjectInputStream;
import java.util.ArrayList;
import Server_ChatHistory.HistoryCatchList.HistoryBean;
import function.ChatBean;
import function.MainServer;
public class HistorySavingThread extends Thread {
HistoryCatchList historyCatch=MainServer.historyCatch;;// 一级缓存,缓存所有聊天记录
static ArrayList list = new ArrayList<>();// 二级缓存储存列表
static HistoryLinkedList forPublic;// 公共聊天缓存
public HistorySavingThread() {
//this.historyCatch =
}
@Override
public void run() {
forPublic = new HistoryLinkedList(-1);
list.add(forPublic);// 公共聊天缓存区创建
synchronized (historyCatch) {
while (true) {
if (historyCatch.isEmpty()) {
try {
historyCatch.wait();
} catch (InterruptedException e) {
System.out.println("历史记录处理线程被唤醒");
}
}
HistoryBean history = historyCatch.takeHsitory();
int[] target = history.target;
ChatBean cb = history.bean;
if (target[0] < 0) {// 意味着是公共聊天
forPublic.add(cb);
} else {// 私人聊天->
HistoryLinkedList tmp = findList(target);
if (tmp == null) {// 创建新的
HistoryLinkedList tmp2=readHistoryOnDisk(""+target[0]+"+"+target[1]);
if(tmp2==null) {
tmp2= new HistoryLinkedList(target);
System.out.println("创建新的聊天缓存记录区");
}
tmp2.add(cb);
list.add(tmp2);
} else {// 加入已有的
tmp.add(cb);
}
}
}
}
}
private HistoryLinkedList readHistoryOnDisk(String fileName) {//
File tmp = new File(fileName);
if (tmp.exists()) {
try {
System.out.println("读取"+fileName+"历史记录中");
ObjectInputStream ois = new ObjectInputStream(new FileInputStream(tmp));
HistoryLinkedList HLL=(HistoryLinkedList) ois.readObject();
return HLL;
} catch (Exception e) {
//e.printStackTrace();
System.out.println("读取历史文件出错!+"+tmp.getAbsolutePath());
return null;
}
} else {
return null;
}
}
public static HistoryLinkedList findList(int target[]) {
if (target.length==1)return forPublic;
for (HistoryLinkedList tmp : list) {
if (tmp.low == target[0] && tmp.high == target[1]) {
return tmp;// 找到ID完全相等则返回
}
}
return null;// 否则返回空
}
}