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