Java版——一个简易的QQ聊天室程序

介绍

该程序是基于C/S架构模式,即服务端/客户端模式(这种架构模式维护起来既耗时又耗人力物力,不过也不是绝对的哈),其中使用了Java多线程中的一些常用API,例如ConcurrentHashMap(并发HashMap)、还有javax.swing包和java.awt包中的一些GUI组件,因为这是一个基于Java的GUI图形界面聊天室,虽然说现在已经很少有人用GUI来开发软件了。

其实有两大因素:

1、开发周期长,图形界面的绘制太耗时,而且移植性也不太好

2、布局繁琐,样式不太美观,如果非要调整的好看,那也要花很大功夫,不利于现代软件的快速开发,因为现在的软件更新迭代太快

 

程序中涉及的功能

1、登录:登录聊天室,每次登录只需填写一个昵称即可

2、群聊:所有登录的用户处于一个聊天室内,大家可以群聊,即每个人发送的消息都会被所有人可见,后续会增加私聊功能和@指定用户

3、服务器端可以看到每个用户登录之后的信息,还可以看到每个人发送的群消息

 

这个程序包含了六个类,分别是:Server.java、ClientState.java、Message.java、Type.java、UserThread.java、Client.java

 

源代码

 Server.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.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.java

 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  *         1、一定要记得时时更新集合中的数据
 26  *         2、一定要记得刷新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 }

 

posted @ 2021-04-14 19:50  没有你哪有我  阅读(1362)  评论(0编辑  收藏  举报