Java版——一個簡易的QQ聊天室程序

介紹

該程序是基於C/S架構模式,即服務端/客戶端模式(這種架構模式維護起來既耗時又耗人力物力,不過也不是絕對的哈),其中使用了Java多線程中的一些經常使用API,例如ConcurrentHashMap(併發HashMap)、還有javax.swing包和java.awt包中的一些GUI組件,由於這是一個基於Java的GUI圖形界面聊天室,雖說如今已經不多有人用GUI來開發軟件了。java

其實有兩大因素:數據庫

一、開發週期長,圖形界面的繪製太耗時,並且移植性也不太好編程

二、佈局繁瑣,樣式不太美觀,若是非要調整的好看,那也要花很大功夫,不利於現代軟件的快速開發,由於如今的軟件更新迭代太快服務器

 

程序中涉及的功能

一、登陸:登陸聊天室,每次登陸只需填寫一個暱稱便可網絡

二、羣聊:全部登陸的用戶處於一個聊天室內,你們能夠羣聊,即每一個人發送的消息都會被全部人可見,後續會增長私聊功能和@指定用戶多線程

三、服務器端能夠看到每一個用戶登陸以後的信息,還能夠看到每一個人發送的羣消息架構

 

這個程序包含了六個類,分別是:Server.java、ClientState.java、Message.java、Type.java、UserThread.java、Client.java併發

 

源代碼

 Server.javaapp

  1 package chat;
  2 
  3 import static java.util.concurrent.Executors.newFixedThreadPool;
  4 import java.awt.Font;
  5 import java.awt.event.ActionEvent;
  6 import java.awt.event.ActionListener;
  7 import java.awt.event.KeyAdapter;
  8 import java.awt.event.KeyEvent;
  9 import java.io.IOException;
 10 import java.io.ObjectOutputStream;
 11 import java.net.ServerSocket;
 12 import java.net.Socket;
 13 import java.util.Map;
 14 import java.util.Set;
 15 import java.util.concurrent.ConcurrentHashMap;
 16 import java.util.concurrent.ExecutorService;
 17 import javax.swing.BorderFactory;
 18 import javax.swing.DefaultListModel;
 19 import javax.swing.JButton;
 20 import javax.swing.JFrame;
 21 import javax.swing.JList;
 22 import javax.swing.JOptionPane;
 23 import javax.swing.JScrollPane;
 24 import javax.swing.JTextArea;
 25 import javax.swing.JTextField;
 26 import javax.swing.SwingUtilities;
 27 import javax.swing.WindowConstants;
 28 
 29 /**
 30  * 單例服務器
 31  *
 32  * 需求:實現服務器與客戶端互發消息
 33  * 步驟:
 34  *       1.開啓服務器
 35  *       2.監聽客戶端鏈接
 36  *       3.提示客戶端鏈接成功
 37  *       4.開啓線程給每一個鏈接該服務器的用戶
 38  *       5.轉發各個用戶的消息
 39  *
 40  * 服務器主要功能:
 41  *         1.廣播全部在線客戶端
 42  *         2.羣聊(轉發一個客戶端的消息到其餘有客戶端上)
 43  *         3.私聊(暫時未寫)
 44  *
 45  * @author 14715
 46  *
 47  */
 48 public class Server {
 49 
 50     // 服務器套接字
 51     private ServerSocket server = null;
 52 
 53     private static Map<String,UserThread> onlineUsers = new ConcurrentHashMap<>();;
 54 
 55     private static JFrame f = null;
 56 
 57     private JButton startBtn = null;
 58 
 59     private JButton stopBtn = null;
 60 
 61     private static JTextArea msgTa = null;
 62 
 63     private static DefaultListModel<String> dlm = null;
 64 
 65     private static JList<?> onlineList = null;
 66 
 67     private static JTextField sendTf = null;
 68 
 69     private JScrollPane spMsg = null;
 70 
 71     private JScrollPane spOnline = null;
 72 
 73     private JButton sendBtn = null;
 74 
 75     private ExecutorService es = newFixedThreadPool(5);
 76 
 77     //標識服務器的狀態,爲false表示關閉狀態,反之,則開啓狀態
 78     private boolean state = false;
 79 
 80     /**
 81      * 啓動服務器
 82      */
 83     private void startServer() {
 84         try {
 85             // 開啓服務
 86             server = new ServerSocket(12345);
 87             //打印信息到控制檯
 88             System.out.println("QQ服務器已啓動------------正在等待客戶端鏈接!!!");
 89             //刷新UI
 90             SwingUtilities.invokeLater(() -> {
 91                 Server.flushUI(null, Type.SERVERSTART, null);
 92             });
 93             //開啓線程,監聽客戶端
 94             //修改標識
 95             state = true;
 96             es.execute(() -> {
 97                 acceptClient();
 98             });
 99             //將啓動服務器按鈕設置爲不可用
100             startBtn.setEnabled(false);
101             //將關閉服務器按鈕設置爲可用
102             stopBtn.setEnabled(true);
103         } catch (IOException e) {
104             JOptionPane.showMessageDialog(f,"服務器已經啓動過了,不可重複啓動","舒適提示",1);
105         }
106     }
107 
108     /**
109      * 關閉服務器
110      * 注意:關閉服務器必須將全部除了繪製UI界面的線程都中止,才中止了服務
111      */
112     public void closeServer() {
113         try {
114             //修改狀態
115             state = false;
116             //關閉服務器套接字
117             server.close();
118             server = null;
119             //將啓動服務器按鈕設置爲可用
120             startBtn.setEnabled(true);
121             //將關閉服務器按鈕設置爲不可用
122             stopBtn.setEnabled(false);
123             //遍歷HashMap
124             Set<String> keys = onlineUsers.keySet();
125             // 關閉全部用戶線程
126             // 不要在遍歷集合的時候去刪除該集合,這樣程序會拋出java.util.ConcurrentModificationException併發修改異常
127             // 注意:這裏咱們必定不能使用之前的HashMap了,而是要使用jdk中爲咱們提供的juc併發包下的ConcurrentHashMap
128             for (String s : keys) {
129                 UserThread ut = (UserThread) onlineUsers.get(s);
130                 //此處要特別注意:絕對不能用線程來判斷爲null,由於調用這個方法時,線程還沒結束。
131                 if (ut != null) {
132                     // 注意:修改標誌必定要首先執行,否則該用戶線程又會繼續等待客戶端發送消息
133                     ut.setStop(true);
134                     // 關閉該線程對應的客戶端套接字
135                     Socket client = ut.getClient();
136                     try {
137                         if (client != null) {
138                             client.close();
139                             System.out.println(client + "已斷開");
140                             ut.setClient(null);
141                         }
142                     } catch (IOException e) {
143                         System.out.println("2222222222222222");
144                         e.printStackTrace();
145                     }
146                     System.out.println(ut.getNickName() + "用戶線程正在被關閉!");
147                 }
148             }
149             // 移除Map集合中的全部用戶線程
150             for (String s : keys) {
151                 onlineUsers.remove(s);
152             }
153             System.out.println("服務器正在關閉-----");
154             //刷新UI
155             SwingUtilities.invokeLater(() -> {
156                 Server.flushUI(null, Type.SERVERCLOSE, null);
157             });
158             System.out.println("服務器已關閉-----");
159         } catch (IOException e) {
160             System.out.println("111111");
161             e.printStackTrace();
162         }
163     }
164     /**
165      * 監聽客戶端鏈接
166      */
167     private void acceptClient() {
168         while (state) {
169             Socket client = null;
170             try {
171                 if (server != null) {
172                     client = server.accept();
173                     // 新一個客戶端進來
174                     // 開啓線程
175                     es.execute(new UserThread(client,onlineUsers));
176                 }
177             } catch (IOException e) {
178                 continue;
179             }
180         }
181     }
182 
183     /**
184      * 初始化界面
185      */
186     public void init() {
187         // 建立JFrame對象
188         f = new JFrame();
189 
190         // 設置窗體基本屬性
191         f.setTitle("QQ服務器");
192         f.setDefaultCloseOperation(WindowConstants.EXIT_ON_CLOSE);
193         f.setBounds(300, 150, 580, 400);
194         f.setResizable(false);
195         f.setLayout(null);
196         f.setVisible(true);
197 
198         //實例化啓動服務器和關閉服務器按鈕
199         startBtn = new JButton("開啓服務器");
200         startBtn.setBounds(50,10,100,35);
201 
202         stopBtn = new JButton("關閉服務器");
203         stopBtn.setBounds(260,10,100,35);
204         stopBtn.setEnabled(false);
205 
206         msgTa = new JTextArea();
207         msgTa.setEditable(false);
208         msgTa.setFont(new Font("微軟雅黑", Font.CENTER_BASELINE, 13));
209 
210         spMsg = new JScrollPane(msgTa);
211         spMsg.setBounds(5, 45, 405, 260);
212         //設置標題邊框
213         spMsg.setBorder(BorderFactory.createTitledBorder("消息列表"));
214 
215         dlm = new DefaultListModel<>();
216 
217         onlineList = new JList<>(dlm);
218 
219         spOnline = new JScrollPane(onlineList);
220         spOnline.setBounds(425, 5, 145, 300);
221         //設置標題邊框
222         spOnline.setBorder(BorderFactory.createTitledBorder("在線列表"));
223 
224         sendTf = new JTextField();
225         sendTf.setBounds(20, 320, 380, 35);
226         sendTf.setFont(new Font("宋體", Font.BOLD, 20));
227 
228         sendBtn = new JButton("發送");
229         sendBtn.setBounds(445, 320, 100, 35);
230 
231         f.add(startBtn);
232         f.add(stopBtn);
233         f.add(spMsg);
234         f.add(spOnline);
235         f.add(sendTf);
236         f.add(sendBtn);
237 
238         //註冊事件
239         regEvent();
240     }
241 
242     /**
243      * 註冊事件
244      */
245     private void regEvent() {
246         //註冊啓動服務器事件
247         startBtn.addActionListener(new ActionListener() {
248             @Override
249             public void actionPerformed(ActionEvent e) {
250                 startServer();
251             }
252         });
253 
254         //關閉服務器事件
255         stopBtn.addActionListener(new ActionListener() {
256             @Override
257             public void actionPerformed(ActionEvent e) {
258                 closeServer();
259             }
260         });
261 
262         //按鈕動做事件
263         sendBtn.addActionListener(new ActionListener() {
264             @Override
265             public void actionPerformed(ActionEvent e) {
266                 //獲取文本框中的即將要發送的消息內容
267                 String content = sendTf.getText().trim();
268                 if (!("".equals(content))) {
269                     serverInform(content);
270                     //清空文本框
271                     sendTf.setText("");
272                 } else {
273                     JOptionPane.showMessageDialog(f, "發送內容不能爲空", "提示", 1);
274                 }
275             }
276         });
277 
278         //文本框鍵盤事件
279         sendTf.addKeyListener(new KeyAdapter()
280         {
281             @Override
282             public void keyPressed(KeyEvent e) {
283                 if (e.getKeyCode() == KeyEvent.VK_ENTER) {
284                     //獲取文本框中的即將要發送的消息內容
285                     String content = sendTf.getText();
286                     if (!("".equals(content))) {
287                         serverInform(content);
288                           //清空文本框
289                         sendTf.setText("");
290                     } else {
291                         JOptionPane.showMessageDialog(f, "發送內容不能爲空", "提示", 1);
292                     }
293                 }
294             }
295         });
296     }
297 
298     /**
299      * 服務器通知全部客戶端消息
300      */
301     private void serverInform(final String msg) {
302         ObjectOutputStream oos = null;
303         try {
304             //判斷服務器是否已啓動
305             if (state) {
306                 if (onlineUsers.size() > 0) {
307                     //遍歷HashMap
308                     Set<String> keys = onlineUsers.keySet();
309                     for (String s : keys) {
310                         UserThread ut = (UserThread)onlineUsers.get(s);
311                         Socket client = ut.getClient();
312                         if (client != null) {
313                             oos = new ObjectOutputStream(client.getOutputStream());
314                             //建立Message對象
315                             Message m = new Message();
316                             m.setInfo("服務器:" + msg);
317                             m.setType(Type.SERVERINFORM);
318                             oos.writeObject(m);
319                             oos.flush();
320                         }
321                     }
322                     SwingUtilities.invokeLater(new Runnable() {
323                         @Override
324                         public void run() {
325                             //刷新UI
326                             flushUI(null, Type.SERVERINFORM, msg);
327                         }
328                     });
329                 } else {
330                     //此時沒有客戶端在線
331                     //彈出提示框提示服務器
332                     SwingUtilities.invokeLater(new Runnable() {
333                         @Override
334                         public void run() {
335                             JOptionPane.showMessageDialog(f, "當前無客戶端在線", "提示", 3);
336                         }
337                     });
338                 }
339             } else {
340                 //彈出提示框提示服務器未啓動
341                 SwingUtilities.invokeLater(new Runnable() {
342                     @Override
343                     public void run() {
344                         JOptionPane.showMessageDialog(f, "服務器未啓動", "提示", 2);
345                     }
346                 });
347             }
348         } catch (IOException e) {
349             e.printStackTrace();
350         }
351     }
352 
353     /**
354      * 發送在線用戶集合給每一個在線的客戶端
355      */
356     public static void sendMap() {
357         ObjectOutputStream oos = null;
358         try {
359             //遍歷Map,響應其餘客戶端
360             Set<String> keys = onlineUsers.keySet();
361             for (String s : keys) {
362                 UserThread temp = (UserThread)onlineUsers.get(s);
363                 if (temp != null) {
364                     oos = new ObjectOutputStream(temp.getClient().getOutputStream());
365                     oos.writeObject(onlineUsers);
366                     oos.flush();
367                 }
368             }
369         } catch (IOException e) {
370             e.printStackTrace();
371         }
372     }
373 
374     /**
375      * 羣發消息
376      * @param from
377      * @param msg
378      */
379     public static void groupSend(final UserThread from,final String msg) {
380         ObjectOutputStream oos = null;
381         try {
382             //遍歷HashMap
383             Set<String> keys = onlineUsers.keySet();
384             for (String s : keys) {
385                 UserThread ut = (UserThread) onlineUsers.get(s);
386                 if (ut != null && ut != from) {
387                     oos = new ObjectOutputStream(ut.getClient().getOutputStream());
388                     //建立Message對象
389                     Message m = new Message();
390                     m.setInfo(from.getNickName() + ":" + msg);
391                     m.setType(Type.GROUPRECEIVE);
392                     oos.writeObject(m);
393                     oos.flush();
394                 }
395             }
396         } catch (IOException e) {
397             e.printStackTrace();
398         }
399     }
400 
401     /**
402      * 刷新UI控件
403      * 注意:服務器關閉屬於特殊狀況,這時候必須將List控件中的全部用戶信息所有移除
404      */
405     public static void flushUI(UserThread u,int state,String msg) {
406         //判斷客戶端的鏈接狀態
407         switch (state) {
408             case Type.SERVERSTART :
409                 // 顯示服務器啓動提示信息
410                 msgTa.append("QQ服務器已啓動----------正在等待客戶端鏈接!!!\n");
411                 break;
412             case Type.SERVERCLOSE :
413                 // 移除List控件中的全部用戶信息
414                 onlineList.removeAll();
415                 // 顯示服務器啓動提示信息
416                 msgTa.append("QQ服務器已關閉------稍後再爲您服務!!!\n");
417                 break;
418             case Type.CONNECT :
419                 msgTa.append("客戶端【" + u.getLocalHost() + "】已鏈接\n");
420                 break;
421             case Type.LOGIN :
422                 msgTa.append("客戶端" + u.getLocalHost() + "【 " + u.getNickName() + "】已登陸\n");
423                 dlm.addElement(u.getLocalHost() + "/" + u.getNickName());
424                 break;
425             case Type.GROUPSEND :
426                 msgTa.append("客戶端" + u.getLocalHost() + "【 " + u.getNickName() + "】羣發了一條消息:" + msg + "\n");
427                 break;
428             case Type.PRIVATESEND :
429                 break;
430             case Type.SERVERINFORM :
431                 msgTa.append("服務器向全部客戶端發送了一條消息:" + msg + "\n");
432                 break;
433             case Type.CLIENTEXIT :
434                 msgTa.append("客戶端【" + u.getLocalHost() + "】已斷開鏈接\n");
435                 dlm.removeElement(u.getLocalHost() + "/" + u.getNickName());
436                 break;
437         }
438         //1.刷新消息面板
439         msgTa.updateUI();
440         //2.更新List面板
441         onlineList.updateUI();
442     }
443 
444     /**
445      * 主方法
446      * @param args
447      */
448     public static void main(String[] args) {
449         Server server = new Server();
450         server.init();
451     }
452 }

ClientState.javasocket

 1 package chat;
 2 
 3 /**
 4  * 客戶端消息常量類
 5  * @author Administrator
 6  *
 7  */
 8 public class ClientState {
 9     
10     //發送的消息
11     public static final int SEND = 0x001;
12     
13     //接收的消息
14     public static final int RECEIVE = 0x002;
15 
16 }

Message.java

 1 package chat;
 2 
 3 import java.io.Serializable;
 4 
 5 
 6 @SuppressWarnings("serial")
 7 public class Message implements Serializable{
 8     
 9     private String from;
10     
11     private String to;
12     
13     private String info;
14     
15     private int type;
16     
17     public Message() {
18         
19     }
20 
21     public Message(String from, String to, String info, int type) {
22         this.from = from;
23         this.to = to;
24         this.info = info;
25         this.type = type;
26     }
27 
28     public String getFrom() {
29         return from;
30     }
31 
32     public void setFrom(String from) {
33         this.from = from;
34     }
35 
36     public String getTo() {
37         return to;
38     }
39 
40     public void setTo(String to) {
41         this.to = to;
42     }
43 
44     public String getInfo() {
45         return info;
46     }
47 
48     public void setInfo(String info) {
49         this.info = info;
50     }
51 
52     public int getType() {
53         return type;
54     }
55 
56     public void setType(int type) {
57         this.type = type;
58     }
59     
60     
61 
62 }

Type.java

 1 package chat;
 2 
 3 import java.io.Serializable;
 4 
 5 /**
 6  * @author Administrator
 7  *
 8  */
 9 @SuppressWarnings("serial")
10 public class Type implements Serializable{
11 
12     public static final int LOGIN = 0x001;
13     
14     public static final int GROUPSEND = 0x002;
15     
16     public static final int GROUPRECEIVE = 0x003;
17     
18     public static final int PRIVATESEND = 0x004;
19     
20     public static final int PRIVATERECEIVE = 0x005;
21     
22     public static final int SERVERINFORM = 0x006;
23     
24     public static final int CLIENTEXIT = 0x007;
25 
26     public static final int CONNECT = 0x008;
27 
28     public static final int SERVERCLOSE = 0x009;
29 
30     public static final int SERVERSTART = 0;
31     
32 }

UserThread.java

  1 package chat;
  2 
  3 import java.io.IOException;
  4 import java.io.ObjectInputStream;
  5 import java.io.Serializable;
  6 import java.net.Socket;
  7 import java.util.Map;
  8 import javax.swing.SwingUtilities;
  9 
 10 /**
 11  * 用戶處理線程
 12  * 功能一:接受用戶的消息
 13  * 功能二:回覆用戶的消息
 14  * 功能三:刷新UI控件
 15  * 
 16  * 用ObjectOutputStream和ObjectInputStream這兩個流對象傳輸時,必定要注意哪些對象是沒有實現Serializable接口,
 17  * 因此在傳輸過程當中,不能被傳輸,會拋異常NotSerilizableException
 18  * 
 19  * 解決辦法:將不能被序列化的對象加上transient關鍵字
 20  * 
 21  * 這次用ObjectOutputStream和ObjectInputStream對象輸入輸出流注意事項:socket對象是不能被序列化得,自身能夠做爲傳輸的工具,可是自身不能被序列化,
 22  * 因此在此特別要注意。本人在這個地方困惑了將近兩天,最後在一篇博客上纔看到有人說socket對象是不能被序列化的,
 23  * 
 24  * 注意:GUI網絡編程的兩個關鍵步驟:
 25  *         一、必定要記得時時更新集合中的數據
 26  *         二、必定要記得刷新UI控件,作到及時響應客戶端
 27  * @author Administrator
 28  *
 29  */
 30 @SuppressWarnings("serial")
 31 public class UserThread implements Runnable,Serializable{
 32 
 33     private transient Socket client;
 34     
 35     private String localHost = null;
 36     
 37     private UserThread user = this;
 38     
 39     private Map<String,UserThread> onlineUsers;
 40     
 41     private String nickName;
 42     
 43     private boolean isStop = false;
 44     
 45     public boolean isStop() {
 46         return isStop;
 47     }
 48 
 49     public void setStop(boolean isStop) {
 50         this.isStop = isStop;
 51     }
 52 
 53     /**
 54      * 處理客戶端線程的構造方法
 55      * @param client
 56      * @param onlineUsers
 57      */
 58     public UserThread(Socket client,Map<String,UserThread> onlineUsers) {
 59         //鏈接成功      
 60         this.client = client;
 61         this.localHost = client.getInetAddress().getHostAddress() + "::" + client.getPort();
 62         this.onlineUsers = onlineUsers; 
 63         // 刷新List
 64         Server.flushUI(this, Type.CONNECT, null);
 65     }
 66     
 67     /**
 68      * 線程方法
 69      */
 70     @Override
 71     public void run() {
 72         while (!isStop) {
 73             if (!hear()) {
 74                 // 進入,表示客戶端本身退出
 75                 // 若是返回false,則跳出循環
 76                 onlineUsers.remove(nickName);
 77                 break;
 78             }
 79         }
 80         // 鏈接斷開緣由 :多是客戶端本身斷開,也多是服務器關閉
 81         // 經過該線程的暱稱鍵名來從Map集合中移除 該鍵值對
 82         // 發送存儲在線用戶的Map集合給每一個在線的客戶端
 83         // 響應客戶端
 84         if (!isStop) {
 85             Server.sendMap();
 86         }
 87         // 響應服務端
 88         SwingUtilities.invokeLater(() -> {
 89             Server.flushUI(user, Type.CLIENTEXIT, null);
 90         });
 91     }
 92     
 93     /**
 94      * 負責接收客戶端發來的全部類型消息
 95      * @return 返回的boolean值標識這客戶端的鏈接狀態
 96      * 若是返回false則標識客戶端已斷開鏈接
 97      * 反之,則標識客戶端沒斷開鏈接
 98      */
 99     private boolean hear() {
100         ObjectInputStream ois = null;
101         try {
102             ois = new ObjectInputStream(client.getInputStream());
103             Object obj = ois.readObject();
104             if (obj instanceof Message) {
105                 Message msg = (Message) obj;
106                 //判斷消息類型
107                 switch (msg.getType()) {
108                     case Type.LOGIN :
109                         String nickName = msg.getInfo();
110                         this.nickName = nickName;
111                         onlineUsers.put(this.nickName, this);
112                         //發送存儲在線用戶的HashMap集合給每一個在線的客戶端
113                         Server.sendMap();
114                         SwingUtilities.invokeLater(() -> {
115                             Server.flushUI(user, Type.LOGIN, null);
116                         });
117                         break;
118                     case Type.GROUPSEND :
119                         final String info = msg.getInfo();
120                         Server.groupSend(this, info);
121                         SwingUtilities.invokeLater(new Runnable() {            
122                             @Override
123                             public void run() {
124                                 Server.flushUI(user, Type.GROUPSEND, info);
125                             }
126                         });
127                         break;
128                     case Type.PRIVATESEND :
129                         break;              
130                 }
131             }
132         } catch (ClassNotFoundException e) {
133             //此時表示客戶端斷開鏈接
134             return false;
135         } catch (IOException e) {
136             //此時表示客戶端斷開鏈接
137             return false;
138         }
139         return true;
140     }
141     
142     public Socket getClient() {
143         return client;
144     }
145    
146     public void setClient(Socket client) {
147         this.client = client;
148     }
149     
150     public String getLocalHost() {
151         return localHost;
152     }
153     
154     public String getNickName() {
155         return nickName;
156     }
157 }

Client.java

  1 package chat;
  2 
  3 import static java.util.concurrent.Executors.newFixedThreadPool;
  4 import java.awt.Font;
  5 import java.awt.event.ActionEvent;
  6 import java.awt.event.ActionListener;
  7 import java.awt.event.KeyAdapter;
  8 import java.awt.event.KeyEvent;
  9 import java.io.IOException;
 10 import java.io.ObjectInputStream;
 11 import java.io.ObjectOutputStream;
 12 import java.net.Socket;
 13 import java.util.Map;
 14 import java.util.Set;
 15 import java.util.concurrent.ExecutorService;
 16 import javax.swing.DefaultComboBoxModel;
 17 import javax.swing.JButton;
 18 import javax.swing.JComboBox;
 19 import javax.swing.JFrame;
 20 import javax.swing.JLabel;
 21 import javax.swing.JOptionPane;
 22 import javax.swing.JScrollPane;
 23 import javax.swing.JTextArea;
 24 import javax.swing.JTextField;
 25 import javax.swing.SwingUtilities;
 26 import javax.swing.WindowConstants;
 27 
 28 /**
 29  * 客戶端
 30  *
 31  * 需求:多客戶端互發消息
 32  * 步驟:
 33  *       1.鏈接服務器
 34  *       2.嘗試鏈接,接收服務器的提示消息
 35  *       3.若成功鏈接,則開始與好友進行聊天
 36  *         若失敗,則嘗試重連
 37  *       4.這裏我採用Socket對象來區分多個客戶端,而沒有鏈接數據庫像QQ同樣每一個客戶端都能用QQ惟一標識,
 38  *           我先作個簡便的聊天工具。實現羣聊
 39  *       5.注意:客戶端刷新UI控件是必需要經過服務器的響應才能實時刷新的。
 40  *
 41  * @author 14715
 42  *
 43  */
 44 public class Client implements Runnable{
 45 
 46     // 客戶端套接字
 47     private Socket client = null;
 48 
 49     private JFrame f = null;
 50     
 51     public static JTextArea msgTa = null;
 52 
 53     private JScrollPane sp = null;
 54 
 55     private JLabel onlineLa = null;
 56     
 57     private DefaultComboBoxModel<String> dcmbModel = null;
 58     
 59     private JComboBox<String> cmbFriends = null;
 60     
 61     private JTextField sendTf = null;
 62 
 63     private JButton sendBtn = null;
 64 
 65     private ExecutorService es = newFixedThreadPool(4);
 66     
 67     private String nickName = null;
 68 
 69     /**
 70      * 開啓客戶端
 71      */
 72     public void startClient() {
 73         try {
 74             // 開啓服務
 75             client = new Socket("127.0.0.1",12345);
 76             String input = null;
 77             while (true) {
 78                 //只有鏈接上服務器,才彈出JOPtionPane的輸入框模式
 79                 input = JOptionPane.showInputDialog("請輸入您的暱稱");
 80                 //判斷用戶輸入
 81                 if (input != null) {
 82                     if (input.equals("")) {
 83                         JOptionPane.showMessageDialog(f, "暱稱不能爲空");
 84                     } else {
 85                         break;
 86                     }
 87                 } else {
 88                     System.exit(0);
 89                 }
 90             }
 91             //開始登陸
 92             login(input);
 93             //初始化界面
 94             init();
 95             //打印信息到控制檯
 96             System.out.println("已成功鏈接上服務器\n");
 97             // 顯示服務器啓動提示信息
 98             msgTa.append("已成功鏈接上服務器,能夠開始與好友聊天啦!\n");
 99             //開啓線程接收服務器端消息的線程
100             es.execute(this);         
101         } catch (IOException e) {
102             JOptionPane.showMessageDialog(f,"未鏈接到遠程服務器","黃色警告",2);
103             //未鏈接到服務器,則退出程序
104             System.exit(0);
105         }
106     }
107 
108     /**
109      * 初始化界面
110      */
111     public void init() {
112         // 建立JFrame對象
113         f = new JFrame();
114 
115         // 設置窗體基本屬性
116         f.setTitle("QQ客戶端--------暱稱:" + nickName);
117         f.setDefaultCloseOperation(WindowConstants.EXIT_ON_CLOSE);
118         f.setBounds(700, 100, 570, 400);
119         f.setResizable(false);
120         f.setLayout(null);
121         f.setVisible(true);
122 
123         msgTa = new JTextArea();
124         msgTa.setEditable(false);
125         msgTa.setFont(new Font("微軟雅黑", Font.BOLD, 20));
126 
127         sp = new JScrollPane(msgTa);
128         sp.setBounds(5, 5, 440, 300);
129         
130         onlineLa = new JLabel("在線用戶");
131         onlineLa.setBounds(475,5,60,15);
132         
133         dcmbModel = new DefaultComboBoxModel<String>();
134         
135         cmbFriends = new JComboBox<String>(dcmbModel);
136         cmbFriends.setBounds(450,30,110,20);
137         
138         sendTf = new JTextField();
139         sendTf.setBounds(20, 320, 390, 35);
140         sendTf.setFont(new Font("宋體", Font.BOLD, 20));
141 
142         sendBtn = new JButton("發送");
143         sendBtn.setBounds(450, 320, 110, 35);
144 
145         f.add(sp);
146         f.add(onlineLa);
147         f.add(cmbFriends);
148         f.add(sendTf);
149         f.add(sendBtn);
150 
151         //註冊事件監聽
152         regEvent();
153     }
154 
155     /**
156      * 註冊事件
157      */
158     private void regEvent() {
159         sendBtn.addActionListener(new ActionListener() {
160             @Override
161             public void actionPerformed(ActionEvent e) {
162                 //判斷是否成功鏈接上服務器
163                 if (client != null) {
164                     //獲取文本框中的即將要發送的消息內容
165                     String content = sendTf.getText().trim();
166                     if (!("".equals(content))) {               
167                         sendGroupMessage(content);
168                         //清空文本框
169                         sendTf.setText("");
170                     } else {
171                         JOptionPane.showMessageDialog(f, "發送內容不能爲空", "提示", 1);
172                     }
173                 } else {
174                     JOptionPane.showMessageDialog(f,"請先鏈接QQ服務器再與好友聊天吧","警告",2);
175                 }
176             }
177         });
178         
179         sendTf.addKeyListener(new KeyAdapter() {
180             @Override
181             public void keyPressed(KeyEvent e) {
182                 //判斷是否成功鏈接上服務器
183                 if (client != null) {
184                     if (e.getKeyCode() == KeyEvent.VK_ENTER) {
185                         //獲取文本框中的即將要發送的消息內容
186                         String content = sendTf.getText().trim();
187                         if (!("".equals(content))) {               
188                             sendGroupMessage(content);
189                             //清空文本框
190                             sendTf.setText("");
191                         } else {
192                             JOptionPane.showMessageDialog(f, "發送內容不能爲空", "提示", 1);
193                         }
194                     }
195                 } else {
196                     JOptionPane.showMessageDialog(f,"請先鏈接QQ服務器再與好友聊天吧","警告",2);
197                 }
198             }
199         });
200     }
201 
202     /**
203      * QQ登陸
204      * @param nickName 登陸暱稱
205      */
206     private void login(String nickName) {
207         ObjectOutputStream oos = null;
208         try {
209             oos = new ObjectOutputStream(client.getOutputStream());
210             //建立Message對象
211             Message m = new Message();
212             m.setInfo(nickName);
213             m.setType(Type.LOGIN);
214             //發送登陸數據
215             oos.writeObject(m);
216             oos.flush();
217             this.nickName = nickName;
218         } catch (IOException e) {
219             e.printStackTrace();
220         }      
221     }
222     
223     /**
224      * 羣發消息給服務器
225      */
226     private void sendGroupMessage(final String msg) {
227         ObjectOutputStream oos = null;
228         try {
229             oos = new ObjectOutputStream(client.getOutputStream());
230             //建立Message對象
231             Message m = new Message();
232             m.setInfo(msg);
233             m.setType(Type.GROUPSEND);
234             oos.writeObject(m);
235             oos.flush();
236             //刷新UI
237             SwingUtilities.invokeLater(new Runnable() {            
238                 @Override
239                 public void run() {
240                     flushUI(msg, Type.GROUPSEND);
241                 }
242             });
243         } catch (IOException e) {
244             e.printStackTrace();
245         }
246     }
247 
248     /**
249      * 接收在線客戶的Map集合
250      */
251     @SuppressWarnings({ "unchecked" })
252     private void receiveMap(Object obj) {
253         //讀取ArrayList集合
254         Map<String, UserThread> onlineUsers = (Map<String, UserThread>) obj;
255         //先將以前的JComboBox控件中的內容所有清空
256         dcmbModel.removeAllElements();
257         //遍歷HashMap
258         Set<String> keys = onlineUsers.keySet();
259         for (String s : keys) {
260             dcmbModel.addElement(s);
261         }
262         SwingUtilities.invokeLater(new Runnable() {
263             @Override
264             public void run() {
265                 cmbFriends.updateUI();
266             }
267         });
268     }
269     
270     /**
271      * 接收到服務器發送來的羣消息
272      * @param obj
273      */
274     private void receiveMessage(Object obj) {
275         Message m = (Message) obj;
276         //獲取消息內容
277         final String info = m.getInfo();
278         //判斷是羣發仍是服務器的通知消息
279         switch (m.getType()) {
280             case Type.GROUPRECEIVE :             
281                 //刷新UI
282                 SwingUtilities.invokeLater(new Runnable() {            
283                     @Override
284                     public void run() {
285                         flushUI(info, Type.GROUPRECEIVE);
286                     }
287                 });
288                 break;
289             case Type.SERVERINFORM : 
290                 //刷新UI
291                 SwingUtilities.invokeLater(new Runnable() {            
292                     @Override
293                     public void run() {
294                         flushUI(info, Type.SERVERINFORM);
295                     }
296                 });
297                 break;
298         }
299     }
300     
301     /**
302      * 讀
303      * @return 返回的boolean值標識這客戶端的鏈接狀態
304      * 若是返回false則標識客戶端已斷開鏈接
305      * 反之,則標識客戶端沒斷開鏈接
306      */
307     private boolean hear() {
308         ObjectInputStream ois = null;
309         try {
310             ois = new ObjectInputStream(client.getInputStream());
311             //readObject()該方法是個阻塞式方法,若是沒有讀取到對象,就會一直等到,不須要考慮其出現null的狀況
312             Object obj = ois.readObject();
313             if (obj instanceof Map) {
314                 //若是接收到的是Map對象
315                 receiveMap(obj);
316             } else if (obj instanceof Message) {
317                 //羣發消息
318                 receiveMessage(obj);
319             }
320         } catch (IOException e) {
321             //此時表示服務器斷開鏈接
322             return false;    
323         } catch (ClassNotFoundException e) {
324             //此時表示服務器斷開鏈接
325             return false;
326         }
327         return true;
328     }
329     
330     /**
331      * 線程方法
332      */
333     @Override
334     public void run() {
335         while (hear()) {
336         }
337         SwingUtilities.invokeLater(new Runnable() {
338             @Override
339             public void run() {
340                 flushUI(null,Type.CLIENTEXIT);
341             }
342         });
343     }
344 
345     /**
346      * 刷新UI控件
347      */
348     private void flushUI(String msg, int state) {
349         switch (state) {
350             case Type.GROUPSEND :
351                 msgTa.append("我:" + msg + "\n");
352                 break;
353             case Type.GROUPRECEIVE :
354                 msgTa.append(msg + "\n");
355                 break;
356             case Type.SERVERINFORM :
357                 msgTa.append(msg + "\n");
358                 break;
359             case Type.CLIENTEXIT :
360                 JOptionPane.showMessageDialog(f, "服務器已斷開", "提示", 0);
361                 //強制退出程序
362                 System.exit(0);
363                 break;
364         } 
365         //刷新消息面板
366         msgTa.updateUI();
367     }
368     
369     /**
370      * 主方法
371      * @param args
372      */
373     public static void main(String[] args) {
374         //建立客戶端對象
375         Client client = new Client();
376         //啓動客戶端,鏈接服務器
377         client.startClient();
378     }
379 }
相關文章
相關標籤/搜索