重构 JAVA 聊天室 —— CS 模式的简单架构实现
前言
自从开始弄起数据挖掘之后,已经很久没写过技术类的博客了,最近学校 JAVA 课设要求实现一个聊天室,想想去年自己已经写了一个了,但是有些要求到的功能我也没实现,但看着原有的代码想了想加功能好像有那么点点难,于是就想着重构,也正好之前有看到别人写的CS架构的代码,感觉扩展性还不错,就试着写了写,写完这个聊天室后,还同时写了一个教学白板,那个白板基于这个聊天室的代码仅仅花了三四个小时就完成了!所以,有一个好的架构还是很重要的。下面就开始介绍我重构后的聊天室(代码已上传到github)
功能介绍
1. 用Java图形用户界面编写聊天室服务器端和客户端, 支持多个客户端连接到一个服务器。每个客户端能够输入账号,包括注册功能。
2. 可以实现群聊(聊天记录显示在所有客户端界面)。
3. 完成好友列表在各个客户端上显示,包括头像和用户名。
4. 可以实现私人聊天,用户可以选择某个其他用户,单独发送信息,同时实现了文件传输,还能发送窗口振动。
5. 服务器能够群发系统消息,能够对用户私发消息,能够强行让某些用户下线。
6. 客户端的上线下线要求能够在其他客户端上面实时刷新。
7.服务器能够查看在线用户和注册用户
(加了下划线的是课设要求之外的)
整体思路
数了数,总共写了27个类,看起来还是蛮多的,但是仔细看一看还是很简单的,我将在下面对其中部分进行解释
工具类
在我之前写的几个socket通信有关的项目里,客户端和服务器传输的都是字符串,而这次,我把要传输的内容封装成了两个类 Response 和 Request,客户端向服务器发起请求,服务器向客户端回应,通过两个类中包含的请求类型来判断需要进行的操作,传输采用ObjectStream。仔细以看其实会发现,这两个类内容很相似
Request
1 public class Request implements Serializable { 2 private static final long serialVersionUID = -1237018286305074249L; 3 /** 请求传送的数据类型 */ 4 private ResponseType type; 5 /** 请求动作 */ 6 private String action; 7 /** 请求域中的数据,name-value */ 8 private Map<String, Object> attributesMap; 9 10 public Request(){ 11 this.attributesMap = new HashMap<String, Object>(); 12 } 13 14 public ResponseType getType() { 15 return type; 16 } 17 18 public void setType(ResponseType type) { 19 this.type = type; 20 } 21 22 public String getAction() { 23 return action; 24 } 25 26 public void setAction(String action) { 27 this.action = action; 28 } 29 30 public Map<String, Object> getAttributesMap() { 31 return attributesMap; 32 } 33 34 public Object getAttribute(String name){ 35 return this.attributesMap.get(name); 36 } 37 38 public void setAttribute(String name, Object value){ 39 this.attributesMap.put(name, value); 40 } 41 42 public void removeAttribute(String name){ 43 this.attributesMap.remove(name); 44 } 45 46 public void clearAttribute(){ 47 this.attributesMap.clear(); 48 } 49 }
Response
1 public class Response implements Serializable { 2 private static final long serialVersionUID = 1689541820872288991L; 3 /** 响应状态 */ 4 private ResponseStatus status; 5 /** 响应数据的类型 */ 6 private ResponseType type; 7 8 private Map<String, Object> dataMap; 9 10 /** 响应输出流 */ 11 private OutputStream outputStream; 12 13 public Response(){ 14 this.status = ResponseStatus.OK; 15 this.dataMap = new HashMap<String, Object>(); 16 } 17 18 19 public ResponseStatus getStatus() { 20 return status; 21 } 22 23 public void setStatus(ResponseStatus status) { 24 this.status = status; 25 } 26 27 public ResponseType getType() { 28 return type; 29 } 30 31 public void setType(ResponseType type) { 32 this.type = type; 33 } 34 35 public Map<String, Object> getDataMap() { 36 return dataMap; 37 } 38 39 public void setDataMap(Map<String, Object> dataMap) { 40 this.dataMap = dataMap; 41 } 42 43 public OutputStream getOutputStream() { 44 return outputStream; 45 } 46 47 public void setOutputStream(OutputStream outputStream) { 48 this.outputStream = outputStream; 49 } 50 51 public void setData(String name, Object value){ 52 this.dataMap.put(name, value); 53 } 54 55 public Object getData(String name){ 56 return this.dataMap.get(name); 57 } 58 59 public void removeData(String name){ 60 this.dataMap.remove(name); 61 } 62 63 public void clearData(){ 64 this.dataMap.clear(); 65 } 66 }
在以上两个类中,传输的内容会包括文件和消息,对于文件和消息,我们需要直到发送者和接受者是谁,需要知道发送时间等等,所以同样封装成了两个类
FileInfo
1 public class FileInfo implements Serializable { 2 private static final long serialVersionUID = -5394575332459969403L; 3 /** 消息接收者 */ 4 private User toUser; 5 /** 消息发送者 */ 6 private User fromUser; 7 /** 源文件名 */ 8 private String srcName; 9 /** 发送时间 */ 10 private Date sendTime; 11 /** 目标地IP */ 12 private String destIp; 13 /** 目标地端口 */ 14 private int destPort; 15 /** 目标文件名 */ 16 private String destName; 17 public User getToUser() { 18 return toUser; 19 } 20 public void setToUser(User toUser) { 21 this.toUser = toUser; 22 } 23 public User getFromUser() { 24 return fromUser; 25 } 26 public void setFromUser(User fromUser) { 27 this.fromUser = fromUser; 28 } 29 public String getSrcName() { 30 return srcName; 31 } 32 public void setSrcName(String srcName) { 33 this.srcName = srcName; 34 } 35 public Date getSendTime() { 36 return sendTime; 37 } 38 public void setSendTime(Date sendTime) { 39 this.sendTime = sendTime; 40 } 41 public String getDestIp() { 42 return destIp; 43 } 44 public void setDestIp(String destIp) { 45 this.destIp = destIp; 46 } 47 public int getDestPort() { 48 return destPort; 49 } 50 public void setDestPort(int destPort) { 51 this.destPort = destPort; 52 } 53 public String getDestName() { 54 return destName; 55 } 56 public void setDestName(String destName) { 57 this.destName = destName; 58 } 59 }
Message
1 public class Message implements Serializable { 2 private static final long serialVersionUID = 1820192075144114657L; 3 /** 消息接收者 */ 4 private User toUser; 5 /** 消息发送者 */ 6 private User fromUser; 7 /** 消息内容 */ 8 private String message; 9 /** 发送时间 */ 10 private Date sendTime; 11 12 13 public User getToUser() { 14 return toUser; 15 } 16 public void setToUser(User toUser) { 17 this.toUser = toUser; 18 } 19 public User getFromUser() { 20 return fromUser; 21 } 22 public void setFromUser(User fromUser) { 23 this.fromUser = fromUser; 24 } 25 public String getMessage() { 26 return message; 27 } 28 public void setMessage(String message) { 29 this.message = message; 30 } 31 32 public Date getSendTime() { 33 return sendTime; 34 } 35 public void setSendTime(Date sendTime) { 36 this.sendTime = sendTime; 37 } 38 }
User
User 类则用于存储用户信息,因为会用于传输,需实现序列化传输
1 public class User implements Serializable { 2 private static final long serialVersionUID = 5942011574971970871L; 3 private long id; 4 private String password; 5 private String nickname; 6 private int head; 7 private char sex; 8 9 public User(String password, String nickname, char sex, int head){ 10 this.password = password; 11 this.sex = sex; 12 this.head = head; 13 if(nickname.equals("")||nickname==null) 14 { 15 this.nickname = "未命名"; 16 }else{ 17 this.nickname = nickname; 18 } 19 } 20 21 public User(long id, String password){ 22 this.id = id; 23 this.password = password; 24 } 25 26 public long getId(){ 27 return id; 28 } 29 30 public void setId(long id){ 31 this.id = id; 32 } 33 34 public void setPassword(String password){ 35 this.password = password; 36 } 37 38 public String getPassword(){ 39 return password; 40 } 41 42 public void setSex(char sex){ 43 this.sex=sex; 44 } 45 46 public char getSex(){ 47 return this.sex; 48 } 49 50 public void setNickname(String nickname){ 51 this.nickname = nickname; 52 } 53 54 public String getNickname(){ 55 return this.nickname; 56 } 57 58 public void setHead(int head){ 59 this.head = head; 60 } 61 62 public int getHead(){ 63 return this.head; 64 } 65 66 public ImageIcon getHeadIcon(){ 67 ImageIcon image = new ImageIcon("images/"+head+".png"); 68 return image; 69 } 70 71 @Override 72 public int hashCode() { 73 final int prime = 31; 74 int result = 1; 75 result = prime * result + head; 76 result = prime * result + (int)(id ^ (id >> 32)); 77 result = prime * result + ((nickname == null) ? 0 : nickname.hashCode()); 78 result = prime * result + ((password == null) ? 0 : password.hashCode()); 79 result = prime * result + sex; 80 return result; 81 } 82 83 @Override 84 public boolean equals(Object obj) { 85 if(this == obj) 86 return true; 87 if(obj == null) 88 return false; 89 if(getClass() != obj.getClass()) 90 return false; 91 User other = (User) obj; 92 if(head != other.head || id != other.id || sex != other.sex) 93 return false; 94 if(nickname == null){ 95 if(other.nickname != null) 96 return false; 97 }else if(!nickname.equals(other.nickname)) 98 return false; 99 if(password == null){ 100 if(other.password != null) 101 return false; 102 }else if(!password.equals(other.password)) 103 return false; 104 return true; 105 } 106 107 @Override 108 public String toString() { 109 return this.getClass().getName() 110 + "[id=" + this.id 111 + ",pwd=" + this.password 112 + ",nickname=" + this.nickname 113 + ",head=" + this.head 114 + ",sex=" + this.sex 115 + "]"; 116 } 117 }
剩余的类就不一一介绍了,如果有需要可以到我的github上找到源代码。
Server端
服务器端的代码用到的类如上所示,其中 entity 中的两个类和 ServerInfoFrame 仅用于界面,所以不会进行介绍。
UserService
用于用户账号管理,预先创建几个账号,然后存到文件中,每次服务器执行时,都会将文件中的账号信息读入,同时新创建的用户账号也会存入到文件中去。
1 public class UserService { 2 private static int idCount = 3; //id 3 4 /** 新增用户 */ 5 public void addUser(User user){ 6 user.setId(++idCount); 7 List<User> users = loadAllUser(); 8 users.add(user); 9 saveAllUser(users); 10 } 11 12 /** 用户登录 */ 13 public User login(long id, String password){ 14 User result = null; 15 List<User> users = loadAllUser(); 16 for (User user : users) { 17 if(id == user.getId() && password.equals(user.getPassword())){ 18 result = user; 19 break; 20 } 21 } 22 return result; 23 } 24 25 /** 根据ID加载用户 */ 26 public User loadUser(long id){ 27 User result = null; 28 List<User> users = loadAllUser(); 29 for (User user : users) { 30 if(id == user.getId()){ 31 result = user; 32 break; 33 } 34 } 35 return result; 36 } 37 38 39 /** 加载所有用户 */ 40 @SuppressWarnings("unchecked") 41 public List<User> loadAllUser() { 42 List<User> list = null; 43 ObjectInputStream ois = null; 44 try { 45 ois = new ObjectInputStream( 46 new FileInputStream( 47 DataBuffer.configProp.getProperty("dbpath"))); 48 49 list = (List<User>)ois.readObject(); 50 } catch (Exception e) { 51 e.printStackTrace(); 52 }finally{ 53 IOUtil.close(ois); 54 } 55 return list; 56 } 57 58 private void saveAllUser(List<User> users) { 59 ObjectOutputStream oos = null; 60 try { 61 oos = new ObjectOutputStream( 62 new FileOutputStream( 63 DataBuffer.configProp.getProperty("dbpath"))); 64 //写回用户信息 65 oos.writeObject(users); 66 oos.flush(); 67 } catch (Exception e) { 68 e.printStackTrace(); 69 }finally{ 70 IOUtil.close(oos); 71 } 72 } 73 74 75 76 /** 初始化几个测试用户 */ 77 public void initUser(){ 78 User user = new User("admin", "Admin", 'm', 0); 79 user.setId(1); 80 81 User user2 = new User("123", "yong", 'm', 1); 82 user2.setId(2); 83 84 User user3 = new User("123", "anni", 'f', 2); 85 user3.setId(3); 86 87 List<User> users = new CopyOnWriteArrayList<User>(); 88 users.add(user); 89 users.add(user2); 90 users.add(user3); 91 92 this.saveAllUser(users); 93 } 94 95 public static void main(String[] args){ 96 new UserService().initUser(); 97 List<User> users = new UserService().loadAllUser(); 98 for (User user : users) { 99 System.out.println(user); 100 } 101 } 102 }
DataBuffer
用于服务器端从文件中读取数据,进行缓存
1 public class DataBuffer { 2 // 服务器端套接字 3 public static ServerSocket serverSocket; 4 //在线用户的IO Map 5 public static Map<Long, OnlineClientIOCache> onlineUserIOCacheMap; 6 //在线用户Map 7 public static Map<Long, User> onlineUsersMap; 8 //服务器配置参数属性集 9 public static Properties configProp; 10 // 已注册用户表的Model 11 public static RegistedUserTableModel registedUserTableModel; 12 // 当前在线用户表的Model 13 public static OnlineUserTableModel onlineUserTableModel; 14 // 当前服务器所在系统的屏幕尺寸 15 public static Dimension screenSize; 16 17 static{ 18 // 初始化 19 onlineUserIOCacheMap = new ConcurrentSkipListMap<Long,OnlineClientIOCache>(); 20 onlineUsersMap = new ConcurrentSkipListMap<Long, User>(); 21 configProp = new Properties(); 22 registedUserTableModel = new RegistedUserTableModel(); 23 onlineUserTableModel = new OnlineUserTableModel(); 24 screenSize = Toolkit.getDefaultToolkit().getScreenSize(); 25 26 // 加载服务器配置文件 27 try { 28 configProp.load(Thread.currentThread() 29 .getContextClassLoader() 30 .getResourceAsStream("serverconfig.properties")); 31 } catch (IOException e) { 32 e.printStackTrace(); 33 } 34 } 35 36 }
RequestProcessor
这时服务器端最重要的一个类了,用于处理客户端发来的消息,并进行回复,对于每一项操作的实现原理无非就是服务器处理内部数据或是向指定客户端发送消息,详细看代码注释
1 public class RequestProcessor implements Runnable { 2 private Socket currentClientSocket; //当前正在请求服务器的客户端Socket 3 4 public RequestProcessor(Socket currentClientSocket){ 5 this.currentClientSocket = currentClientSocket; 6 } 7 8 public void run() { 9 boolean flag = true; //是否不间断监听 10 try{ 11 OnlineClientIOCache currentClientIOCache = new OnlineClientIOCache( 12 new ObjectInputStream(currentClientSocket.getInputStream()), 13 new ObjectOutputStream(currentClientSocket.getOutputStream())); 14 while(flag){ //不停地读取客户端发过来的请求对象 15 //从请求输入流中读取到客户端提交的请求对象 16 Request request = (Request)currentClientIOCache.getOis().readObject(); 17 System.out.println("Server读取了客户端的请求:" + request.getAction()); 18 19 String actionName = request.getAction(); //获取请求中的动作 20 if(actionName.equals("userRegiste")){ //用户注册 21 registe(currentClientIOCache, request); 22 }else if(actionName.equals("userLogin")){ //用户登录 23 login(currentClientIOCache, request); 24 }else if("exit".equals(actionName)){ //请求断开连接 25 flag = logout(currentClientIOCache, request); 26 }else if("chat".equals(actionName)){ //聊天 27 chat(request); 28 }else if("shake".equals(actionName)){ //振动 29 shake(request); 30 }else if("toSendFile".equals(actionName)){ //准备发送文件 31 toSendFile(request); 32 }else if("agreeReceiveFile".equals(actionName)){ //同意接收文件 33 agreeReceiveFile(request); 34 }else if("refuseReceiveFile".equals(actionName)){ //拒绝接收文件 35 refuseReceiveFile(request); 36 } 37 } 38 }catch(Exception e){ 39 e.printStackTrace(); 40 } 41 } 42 43 /** 拒绝接收文件 */ 44 private void refuseReceiveFile(Request request) throws IOException { 45 FileInfo sendFile = (FileInfo)request.getAttribute("sendFile"); 46 Response response = new Response(); //创建一个响应对象 47 response.setType(ResponseType.REFUSERECEIVEFILE); 48 response.setData("sendFile", sendFile); 49 response.setStatus(ResponseStatus.OK); 50 //向请求方的输出流输出响应 51 OnlineClientIOCache ocic = DataBuffer.onlineUserIOCacheMap.get(sendFile.getFromUser().getId()); 52 this.sendResponse(ocic, response); 53 } 54 55 /** 同意接收文件 */ 56 private void agreeReceiveFile(Request request) throws IOException { 57 FileInfo sendFile = (FileInfo)request.getAttribute("sendFile"); 58 //向请求方(发送方)的输出流输出响应 59 Response response = new Response(); //创建一个响应对象 60 response.setType(ResponseType.AGREERECEIVEFILE); 61 response.setData("sendFile", sendFile); 62 response.setStatus(ResponseStatus.OK); 63 OnlineClientIOCache sendIO = DataBuffer.onlineUserIOCacheMap.get(sendFile.getFromUser().getId()); 64 this.sendResponse(sendIO, response); 65 66 //向接收方发出接收文件的响应 67 Response response2 = new Response(); //创建一个响应对象 68 response2.setType(ResponseType.RECEIVEFILE); 69 response2.setData("sendFile", sendFile); 70 response2.setStatus(ResponseStatus.OK); 71 OnlineClientIOCache receiveIO = DataBuffer.onlineUserIOCacheMap.get(sendFile.getToUser().getId()); 72 this.sendResponse(receiveIO, response2); 73 } 74 75 /** 客户端退出 */ 76 public boolean logout(OnlineClientIOCache oio, Request request) throws IOException{ 77 System.out.println(currentClientSocket.getInetAddress().getHostAddress() 78 + ":" + currentClientSocket.getPort() + "走了"); 79 80 User user = (User)request.getAttribute("user"); 81 //把当前上线客户端的IO从Map中删除 82 DataBuffer.onlineUserIOCacheMap.remove(user.getId()); 83 //从在线用户缓存Map中删除当前用户 84 DataBuffer.onlineUsersMap.remove(user.getId()); 85 86 Response response = new Response(); //创建一个响应对象 87 response.setType(ResponseType.LOGOUT); 88 response.setData("logoutUser", user); 89 oio.getOos().writeObject(response); //把响应对象往客户端写 90 oio.getOos().flush(); 91 currentClientSocket.close(); //关闭这个客户端Socket 92 93 DataBuffer.onlineUserTableModel.remove(user.getId()); //把当前下线用户从在线用户表Model中删除 94 iteratorResponse(response);//通知所有其它在线客户端 95 96 return false; //断开监听 97 } 98 /** 注册 */ 99 public void registe(OnlineClientIOCache oio, Request request) throws IOException { 100 User user = (User)request.getAttribute("user"); 101 UserService userService = new UserService(); 102 userService.addUser(user); 103 104 Response response = new Response(); //创建一个响应对象 105 response.setStatus(ResponseStatus.OK); 106 response.setData("user", user); 107 108 oio.getOos().writeObject(response); //把响应对象往客户端写 109 oio.getOos().flush(); 110 111 //把新注册用户添加到RegistedUserTableModel中 112 DataBuffer.registedUserTableModel.add(new String[]{ 113 String.valueOf(user.getId()), 114 user.getPassword(), 115 user.getNickname(), 116 String.valueOf(user.getSex()) 117 }); 118 } 119 120 /** 登录 */ 121 public void login(OnlineClientIOCache currentClientIO, Request request) throws IOException { 122 String idStr = (String)request.getAttribute("id"); 123 String password = (String) request.getAttribute("password"); 124 UserService userService = new UserService(); 125 User user = userService.login(Long.parseLong(idStr), password); 126 127 Response response = new Response(); //创建一个响应对象 128 if(null != user){ 129 if(DataBuffer.onlineUsersMap.containsKey(user.getId())){ //用户已经登录了 130 response.setStatus(ResponseStatus.OK); 131 response.setData("msg", "该 用户已经在别处上线了!"); 132 currentClientIO.getOos().writeObject(response); //把响应对象往客户端写 133 currentClientIO.getOos().flush(); 134 }else { //正确登录 135 DataBuffer.onlineUsersMap.put(user.getId(), user); //添加到在线用户 136 137 //设置在线用户 138 response.setData("onlineUsers", 139 new CopyOnWriteArrayList<User>(DataBuffer.onlineUsersMap.values())); 140 141 response.setStatus(ResponseStatus.OK); 142 response.setData("user", user); 143 currentClientIO.getOos().writeObject(response); //把响应对象往客户端写 144 currentClientIO.getOos().flush(); 145 146 //通知其它用户有人上线了 147 Response response2 = new Response(); 148 response2.setType(ResponseType.LOGIN); 149 response2.setData("loginUser", user); 150 iteratorResponse(response2); 151 152 //把当前上线的用户IO添加到缓存Map中 153 DataBuffer.onlineUserIOCacheMap.put(user.getId(),currentClientIO); 154 155 //把当前上线用户添加到OnlineUserTableModel中 156 DataBuffer.onlineUserTableModel.add( 157 new String[]{String.valueOf(user.getId()), 158 user.getNickname(), 159 String.valueOf(user.getSex())}); 160 } 161 }else{ //登录失败 162 response.setStatus(ResponseStatus.OK); 163 response.setData("msg", "账号或密码不正确!"); 164 currentClientIO.getOos().writeObject(response); 165 currentClientIO.getOos().flush(); 166 } 167 } 168 169 /** 聊天 */ 170 public void chat(Request request) throws IOException { 171 Message msg = (Message)request.getAttribute("msg"); 172 Response response = new Response(); 173 response.setStatus(ResponseStatus.OK); 174 response.setType(ResponseType.CHAT); 175 response.setData("txtMsg", msg); 176 177 if(msg.getToUser() != null){ //私聊:只给私聊的对象返回响应 178 OnlineClientIOCache io = DataBuffer.onlineUserIOCacheMap.get(msg.getToUser().getId()); 179 sendResponse(io, response); 180 }else{ //群聊:给除了发消息的所有客户端都返回响应 181 for(Long id : DataBuffer.onlineUserIOCacheMap.keySet()){ 182 if(msg.getFromUser().getId() == id ){ continue; } 183 sendResponse(DataBuffer.onlineUserIOCacheMap.get(id), response); 184 } 185 } 186 } 187 188 /*广播*/ 189 public static void board(String str) throws IOException { 190 User user = new User(1,"admin"); 191 Message msg = new Message(); 192 msg.setFromUser(user); 193 msg.setSendTime(new Date()); 194 195 DateFormat df = new SimpleDateFormat("HH:mm:ss"); 196 StringBuffer sb = new StringBuffer(); 197 sb.append(" ").append(df.format(msg.getSendTime())).append(" "); 198 sb.append("系统通知\n "+str+"\n"); 199 msg.setMessage(sb.toString()); 200 201 Response response = new Response(); 202 response.setStatus(ResponseStatus.OK); 203 response.setType(ResponseType.BOARD); 204 response.setData("txtMsg", msg); 205 206 for (Long id : DataBuffer.onlineUserIOCacheMap.keySet()) { 207 sendResponse_sys(DataBuffer.onlineUserIOCacheMap.get(id), response); 208 } 209 } 210 211 /*踢除用户*/ 212 public static void remove(User user_) throws IOException{ 213 User user = new User(1,"admin"); 214 Message msg = new Message(); 215 msg.setFromUser(user); 216 msg.setSendTime(new Date()); 217 msg.setToUser(user_); 218 219 StringBuffer sb = new StringBuffer(); 220 DateFormat df = new SimpleDateFormat("HH:mm:ss"); 221 sb.append(" ").append(df.format(msg.getSendTime())).append(" "); 222 sb.append("系统通知您\n "+"您被强制下线"+"\n"); 223 msg.setMessage(sb.toString()); 224 225 Response response = new Response(); 226 response.setStatus(ResponseStatus.OK); 227 response.setType(ResponseType.REMOVE); 228 response.setData("txtMsg", msg); 229 230 OnlineClientIOCache io = DataBuffer.onlineUserIOCacheMap.get(msg.getToUser().getId()); 231 sendResponse_sys(io, response); 232 } 233 234 /*私信*/ 235 public static void chat_sys(String str,User user_) throws IOException{ 236 User user = new User(1,"admin"); 237 Message msg = new Message(); 238 msg.setFromUser(user); 239 msg.setSendTime(new Date()); 240 msg.setToUser(user_); 241 242 DateFormat df = new SimpleDateFormat("HH:mm:ss"); 243 StringBuffer sb = new StringBuffer(); 244 sb.append(" ").append(df.format(msg.getSendTime())).append(" "); 245 sb.append("系统通知您\n "+str+"\n"); 246 msg.setMessage(sb.toString()); 247 248 Response response = new Response(); 249 response.setStatus(ResponseStatus.OK); 250 response.setType(ResponseType.CHAT); 251 response.setData("txtMsg", msg); 252 253 OnlineClientIOCache io = DataBuffer.onlineUserIOCacheMap.get(msg.getToUser().getId()); 254 sendResponse_sys(io, response); 255 } 256 257 /** 发送振动 */ 258 public void shake(Request request)throws IOException { 259 Message msg = (Message) request.getAttribute("msg"); 260 261 DateFormat df = new SimpleDateFormat("HH:mm:ss"); 262 StringBuffer sb = new StringBuffer(); 263 sb.append(" ").append(msg.getFromUser().getNickname()) 264 .append("(").append(msg.getFromUser().getId()).append(") ") 265 .append(df.format(msg.getSendTime())).append("\n 给您发送了一个窗口抖动\n"); 266 msg.setMessage(sb.toString()); 267 268 Response response = new Response(); 269 response.setStatus(ResponseStatus.OK); 270 response.setType(ResponseType.SHAKE); 271 response.setData("ShakeMsg", msg); 272 273 OnlineClientIOCache io = DataBuffer.onlineUserIOCacheMap.get(msg.getToUser().getId()); 274 sendResponse(io, response); 275 } 276 277 /** 准备发送文件 */ 278 public void toSendFile(Request request)throws IOException{ 279 Response response = new Response(); 280 response.setStatus(ResponseStatus.OK); 281 response.setType(ResponseType.TOSENDFILE); 282 FileInfo sendFile = (FileInfo)request.getAttribute("file"); 283 response.setData("sendFile", sendFile); 284 //给文件接收方转发文件发送方的请求 285 OnlineClientIOCache ioCache = DataBuffer.onlineUserIOCacheMap.get(sendFile.getToUser().getId()); 286 sendResponse(ioCache, response); 287 } 288 289 /** 给所有在线客户都发送响应 */ 290 private void iteratorResponse(Response response) throws IOException { 291 for(OnlineClientIOCache onlineUserIO : DataBuffer.onlineUserIOCacheMap.values()){ 292 ObjectOutputStream oos = onlineUserIO.getOos(); 293 oos.writeObject(response); 294 oos.flush(); 295 } 296 } 297 298 /** 向指定客户端IO的输出流中输出指定响应 */ 299 private void sendResponse(OnlineClientIOCache onlineUserIO, Response response)throws IOException { 300 ObjectOutputStream oos = onlineUserIO.getOos(); 301 oos.writeObject(response); 302 oos.flush(); 303 } 304 305 /** 向指定客户端IO的输出流中输出指定响应 */ 306 private static void sendResponse_sys(OnlineClientIOCache onlineUserIO, Response response)throws IOException { 307 ObjectOutputStream oos = onlineUserIO.getOos(); 308 oos.writeObject(response); 309 oos.flush(); 310 } 311 }
Client端
个人感觉做这类项目时,难点是在客户端,之前考虑了很久关于界面的切换,因为涉及到了登陆界面、注册界面、聊天界面,所以如何将客户端的socket与这几个界面联系起来是个值得思考的问题。同时,也思考了好久好友列表的展示方法,最后想到了TIM。下面介绍一下其中的几个类
ClientThread
客户端线程,一个线程表示一个用户,处理服务器发来的消息,在里面用了 currentFrame 这个变量来表示当前窗口。
1 public class ClientThread extends Thread { 2 private JFrame currentFrame; //当前窗体 3 4 public ClientThread(JFrame frame){ 5 currentFrame = frame; 6 } 7 8 public void run() { 9 try { 10 while (DataBuffer.clientSeocket.isConnected()) { 11 Response response = (Response) DataBuffer.ois.readObject(); 12 ResponseType type = response.getType(); 13 14 System.out.println("获取了响应内容:" + type); 15 if (type == ResponseType.LOGIN) { 16 User newUser = (User)response.getData("loginUser"); 17 DataBuffer.onlineUserListModel.addElement(newUser); 18 19 ChatFrame.onlineCountLbl.setText( 20 "在线用户列表("+ DataBuffer.onlineUserListModel.getSize() +")"); 21 ClientUtil.appendTxt2MsgListArea("【系统消息】用户"+newUser.getNickname() + "上线了!\n"); 22 }else if(type == ResponseType.LOGOUT){ 23 User newUser = (User)response.getData("logoutUser"); 24 DataBuffer.onlineUserListModel.removeElement(newUser); 25 26 ChatFrame.onlineCountLbl.setText( 27 "在线用户列表("+ DataBuffer.onlineUserListModel.getSize() +")"); 28 ClientUtil.appendTxt2MsgListArea("【系统消息】用户"+newUser.getNickname() + "下线了!\n"); 29 30 }else if(type == ResponseType.CHAT){ //聊天 31 Message msg = (Message)response.getData("txtMsg"); 32 ClientUtil.appendTxt2MsgListArea(msg.getMessage()); 33 }else if(type == ResponseType.SHAKE){ //振动 34 Message msg = (Message)response.getData("ShakeMsg"); 35 ClientUtil.appendTxt2MsgListArea(msg.getMessage()); 36 new JFrameShaker(this.currentFrame).startShake(); 37 }else if(type == ResponseType.TOSENDFILE){ //准备发送文件 38 toSendFile(response); 39 }else if(type == ResponseType.AGREERECEIVEFILE){ //对方同意接收文件 40 sendFile(response); 41 }else if(type == ResponseType.REFUSERECEIVEFILE){ //对方拒绝接收文件 42 ClientUtil.appendTxt2MsgListArea("【文件消息】对方拒绝接收,文件发送失败!\n"); 43 }else if(type == ResponseType.RECEIVEFILE){ //开始接收文件 44 receiveFile(response); 45 }else if(type == ResponseType.BOARD){ 46 Message msg = (Message)response.getData("txtMsg"); 47 ClientUtil.appendTxt2MsgListArea(msg.getMessage()); 48 }else if(type == ResponseType.REMOVE){ 49 ChatFrame.remove(); 50 } 51 } 52 } catch (IOException e) { 53 //e.printStackTrace(); 54 } catch (ClassNotFoundException e) { 55 e.printStackTrace(); 56 } 57 } 58 59 /** 发送文件 */ 60 private void sendFile(Response response) { 61 final FileInfo sendFile = (FileInfo)response.getData("sendFile"); 62 63 BufferedInputStream bis = null; 64 BufferedOutputStream bos = null; 65 Socket socket = null; 66 try { 67 socket = new Socket(sendFile.getDestIp(),sendFile.getDestPort());//套接字连接 68 bis = new BufferedInputStream(new FileInputStream(sendFile.getSrcName()));//文件读入 69 bos = new BufferedOutputStream(socket.getOutputStream());//文件写出 70 71 byte[] buffer = new byte[1024]; 72 int n = -1; 73 while ((n = bis.read(buffer)) != -1){ 74 bos.write(buffer, 0, n); 75 } 76 bos.flush(); 77 synchronized (this) { 78 ClientUtil.appendTxt2MsgListArea("【文件消息】文件发送完毕!\n"); 79 } 80 } catch (IOException e) { 81 e.printStackTrace(); 82 }finally{ 83 IOUtil.close(bis,bos); 84 SocketUtil.close(socket); 85 } 86 } 87 88 /** 接收文件 */ 89 private void receiveFile(Response response) { 90 final FileInfo sendFile = (FileInfo)response.getData("sendFile"); 91 92 BufferedInputStream bis = null; 93 BufferedOutputStream bos = null; 94 ServerSocket serverSocket = null; 95 Socket socket = null; 96 try { 97 serverSocket = new ServerSocket(sendFile.getDestPort()); 98 socket = serverSocket.accept(); //接收 99 bis = new BufferedInputStream(socket.getInputStream());//缓冲读 100 bos = new BufferedOutputStream(new FileOutputStream(sendFile.getDestName()));//缓冲写出 101 102 byte[] buffer = new byte[1024]; 103 int n = -1; 104 while ((n = bis.read(buffer)) != -1){ 105 bos.write(buffer, 0, n); 106 } 107 bos.flush(); 108 synchronized (this) { 109 ClientUtil.appendTxt2MsgListArea("【文件消息】文件接收完毕!存放在[" 110 + sendFile.getDestName()+"]\n"); 111 } 112 113 } catch (IOException e) { 114 e.printStackTrace(); 115 }finally{ 116 IOUtil.close(bis,bos); 117 SocketUtil.close(socket); 118 SocketUtil.close(serverSocket); 119 } 120 } 121 122 /** 准备发送文件 */ 123 private void toSendFile(Response response) { 124 FileInfo sendFile = (FileInfo)response.getData("sendFile"); 125 126 String fromName = sendFile.getFromUser().getNickname() 127 + "(" + sendFile.getFromUser().getId() + ")"; 128 String fileName = sendFile.getSrcName() 129 .substring(sendFile.getSrcName().lastIndexOf(File.separator)+1); 130 131 int select = JOptionPane.showConfirmDialog(this.currentFrame, 132 fromName + " 向您发送文件 [" + fileName+ "]!\n同意接收吗?", 133 "接收文件", JOptionPane.YES_NO_OPTION); 134 try { 135 Request request = new Request(); 136 request.setAttribute("sendFile", sendFile); 137 138 if (select == JOptionPane.YES_OPTION) { 139 JFileChooser jfc = new JFileChooser(); 140 jfc.setSelectedFile(new File(fileName)); 141 int result = jfc.showSaveDialog(this.currentFrame); 142 143 if (result == JFileChooser.APPROVE_OPTION){ 144 //设置目的地文件名 145 sendFile.setDestName(jfc.getSelectedFile().getCanonicalPath()); 146 //设置目标地的IP和接收文件的端口 147 sendFile.setDestIp(DataBuffer.ip); 148 sendFile.setDestPort(DataBuffer.RECEIVE_FILE_PORT); 149 150 request.setAction("agreeReceiveFile"); 151 // receiveFile(response); 152 ClientUtil.appendTxt2MsgListArea("【文件消息】您已同意接收来自 " 153 + fromName +" 的文件,正在接收文件 ...\n"); 154 } else { 155 request.setAction("refuseReceiveFile"); 156 ClientUtil.appendTxt2MsgListArea("【文件消息】您已拒绝接收来自 " 157 + fromName +" 的文件!\n"); 158 } 159 } else { 160 request.setAction("refuseReceiveFile"); 161 ClientUtil.appendTxt2MsgListArea("【文件消息】您已拒绝接收来自 " 162 + fromName +" 的文件!\n"); 163 } 164 165 ClientUtil.sendTextRequest2(request); 166 } catch (IOException e) { 167 e.printStackTrace(); 168 } 169 } 170 }
ClientUtil
用于客户端向服务器发送消息
1 public class ClientUtil { 2 3 /** 发送请求对象,主动接收响应 */ 4 public static Response sendTextRequest(Request request) throws IOException { 5 Response response = null; 6 try { 7 // 发送请求 8 DataBuffer.oos.writeObject(request); 9 DataBuffer.oos.flush(); 10 System.out.println("客户端发送了请求对象:" + request.getAction()); 11 12 if(!"exit".equals(request.getAction())){ 13 // 获取响应 14 response = (Response) DataBuffer.ois.readObject(); 15 System.out.println("客户端获取到了响应对象:" + response.getStatus()); 16 }else{ 17 System.out.println("客户端断开连接了"); 18 } 19 } catch (IOException e) { 20 throw e; 21 } catch (ClassNotFoundException e) { 22 e.printStackTrace(); 23 } 24 return response; 25 } 26 27 /** 发送请求对象,不主动接收响应 */ 28 public static void sendTextRequest2(Request request) throws IOException { 29 try { 30 DataBuffer.oos.writeObject(request); // 发送请求 31 DataBuffer.oos.flush(); 32 System.out.println("客户端发送了请求对象:" + request.getAction()); 33 } catch (IOException e) { 34 throw e; 35 } 36 } 37 38 /** 把指定文本添加到消息列表文本域中 */ 39 public static void appendTxt2MsgListArea(String txt) { 40 ChatFrame.msgListArea.append(txt); 41 //把光标定位到文本域的最后一行 42 ChatFrame.msgListArea.setCaretPosition(ChatFrame.msgListArea.getDocument().getLength()); 43 } 44 }
总结
大体上的细节我就介绍这些,剩下的大部分都是界面相关的代码,我把整个项目放到github上了,感觉现在用的这个框架可以适应学校内布置的涉及到CS架构的一切任务,学会了,别人要好几天搞定的自己几个小时就行了,而且看起来还会比别人的舒服的多。下一篇将会介绍利用这个框架实现另一个项目——教学白板。