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 }