重構 JAVA 聊天室 —— CS 模式的簡單架構實現

iTryagain發表於2019-06-05

前言

自從開始弄起資料探勘之後,已經很久沒寫過技術類的部落格了,最近學校 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架構的一切任務,學會了,別人要好幾天搞定的自己幾個小時就行了,而且看起來還會比別人的舒服的多。下一篇將會介紹利用這個框架實現另一個專案——教學白板。

 

相關文章