重构 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 }

Request

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 }

Response

在以上两个类中,传输的内容会包括文件和消息,对于文件和消息,我们需要直到发送者和接受者是谁,需要知道发送时间等等,所以同样封装成了两个类

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 }

FileInfo

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 }

Message

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 }

User

剩余的类就不一一介绍了,如果有需要可以到我的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 }

UserService

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 }

DataBuffer

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 }

RequestProcessor

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 }

ClientThread

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 }

ClientUtil

总结

大体上的细节我就介绍这些,剩下的大部分都是界面相关的代码,我把整个项目放到github上了,感觉现在用的这个框架可以适应学校内布置的涉及到CS架构的一切任务,学会了,别人要好几天搞定的自己几个小时就行了,而且看起来还会比别人的舒服的多。下一篇将会介绍利用这个框架实现另一个项目——教学白板。

原文地址:https://www.cnblogs.com/csu-lmw/p/10981374.html

时间: 2024-10-04 05:17:08

重构 JAVA 聊天室 —— CS 模式的简单架构实现的相关文章

[Java聊天室server]实战之二 监听类

前言 学习不论什么一个稍有难度的技术,要对其有充分理性的分析,之后果断做出决定---->也就是人们常说的"多谋善断":本系列尽管涉及的是socket相关的知识,但学习之前,更想和广大程序猿分享的是一种心境:学习是一个循序渐进的过程,心态应该随时调节,保持戒骄戒躁的状态.比方近期在看网易公开课MIT<算法导论>,老师提到,学习算法之前要计算机数学+离散数学+概率论等课程的知识,所以一直学不好算法的程序猿最好还是从基础入手,这都是中国式教育惹的祸啊!(此处省略一万字...

[Java聊天室服务器]实战之五 读写循环(服务端)

前言 学习任何一个稍有难度的技术,要对其有充分理性的分析,之后果断做出决定---->也就是人们常说的"多谋善断":本系列虽然涉及的是socket相关的知识,但学习之前,更想和广大程序员分享的是一种心境:学习是一个循序渐进的过程,心态应该随时调节,保持戒骄戒躁的状态.比如最近在看网易公开课MIT<算法导论>,老师提到,学习算法之前要计算机数学+离散数学+概率论等课程的知识,所以一直学不好算法的程序员不妨从基础入手,这都是中国式教育惹的祸啊!(此处省略一万字......)

[Java聊天室服务器]实战之八 读写循环(客户端)

前言 学习任何一个稍有难度的技术,要对其有充分理性的分析,之后果断做出决定---->也就是人们常说的"多谋善断":本系列虽然涉及的是socket相关的知识,但学习之前,更想和广大程序员分享的是一种心境:学习是一个循序渐进的过程,心态应该随时调节,保持戒骄戒躁的状态.比如最近在看网易公开课MIT<算法导论>,老师提到,学习算法之前要计算机数学+离散数学+概率论等课程的知识,所以一直学不好算法的程序员不妨从基础入手,这都是中国式教育惹的祸啊!(此处省略一万字......)

[Java聊天室服务器]实战之四 线程类

前言 学习任何一个稍有难度的技术,要对其有充分理性的分析,之后果断做出决定---->也就是人们常说的"多谋善断":本系列虽然涉及的是socket相关的知识,但学习之前,更想和广大程序员分享的是一种心境:学习是一个循序渐进的过程,心态应该随时调节,保持戒骄戒躁的状态.比如最近在看网易公开课MIT<算法导论>,老师提到,学习算法之前要计算机数学+离散数学+概率论等课程的知识,所以一直学不好算法的程序员不妨从基础入手,这都是中国式教育惹的祸啊!(此处省略一万字......)

[Java聊天室服务器]实战之七 客户端类

前言 学习任何一个稍有难度的技术,要对其有充分理性的分析,之后果断做出决定---->也就是人们常说的"多谋善断":本系列虽然涉及的是socket相关的知识,但学习之前,更想和广大程序员分享的是一种心境:学习是一个循序渐进的过程,心态应该随时调节,保持戒骄戒躁的状态.比如最近在看网易公开课MIT<算法导论>,老师提到,学习算法之前要计算机数学+离散数学+概率论等课程的知识,所以一直学不好算法的程序员不妨从基础入手,这都是中国式教育惹的祸啊!(此处省略一万字......)

Java聊天室[长轮询]

今天看到有人分享java实现的聊天室,想起很久以前还在热衷于java的时候也做过一个web聊天室,不拿出来晒晒,可能再也不为人知了,单纯是一个兴趣作品,稳定性不好,也没有考虑连接数和并发的问题,拿出来博大家一笑吧,项目我已改为maven管理;     有一些没有修复的bug,比如SesseionManager 里的sessionCache 会只增不减等等,每一个用户一个Session实例,一个消息buffer(MessageQueue)来缓存未收到的消息,有SessionManager来管理,D

[Java聊天室服务器]实战之一 开篇介绍

前言 学习任何一个稍有难度的技术,要对其有充分理性的分析,之后果断做出决定---->也就是人们常说的"多谋善断":本系列虽然涉及的是socket相关的知识,但学习之前,更想和广大程序员分享的是一种心境:学习是一个循序渐进的过程,心态应该随时调节,保持戒骄戒躁的状态.比如最近在看网易公开课MIT<算法导论>,老师提到,学习算法之前要计算机数学+离散数学+概率论等课程的知识,所以一直学不好算法的程序员不妨从基础入手,这都是中国式教育惹的祸啊!(此处省略一万字......)

[Java聊天室服务器]实战之二 监听类

前言 学习任何一个稍有难度的技术,要对其有充分理性的分析,之后果断做出决定---->也就是人们常说的"多谋善断":本系列虽然涉及的是socket相关的知识,但学习之前,更想和广大程序员分享的是一种心境:学习是一个循序渐进的过程,心态应该随时调节,保持戒骄戒躁的状态.比如最近在看网易公开课MIT<算法导论>,老师提到,学习算法之前要计算机数学+离散数学+概率论等课程的知识,所以一直学不好算法的程序员不妨从基础入手,这都是中国式教育惹的祸啊!(此处省略一万字......)

[Java聊天室服务器]实战之三 接收循环

前言 学习任何一个稍有难度的技术,要对其有充分理性的分析,之后果断做出决定---->也就是人们常说的"多谋善断":本系列虽然涉及的是socket相关的知识,但学习之前,更想和广大程序员分享的是一种心境:学习是一个循序渐进的过程,心态应该随时调节,保持戒骄戒躁的状态.比如最近在看网易公开课MIT<算法导论>,老师提到,学习算法之前要计算机数学+离散数学+概率论等课程的知识,所以一直学不好算法的程序员不妨从基础入手,这都是中国式教育惹的祸啊!(此处省略一万字......)