本文共 19721 字,大约阅读时间需要 65 分钟。
首先,你需要了解什么是缓存区(buffer)、通道(channel)、选择器(selector)、TCP协议、java组件Swing(这玩意我以为不会,需要用到什么百度查查就ok)。
其次对java网络编程socket有过简单的应用,起码有过认识,这样在看demo可能会理解更快!
最后,说到这里,先放最后的效果图吧,页面设计一般,请亲喷。
如上图所示,分别是服务端页面和客户端页面,其中服务端分为“服务器配置”、“在线用户列表”、“消息显示区”、“发送消息区”,客户端页面设计差不多,但是在去连接服务端时需要进行用户名和密码的校验,这算是一个基本的功能。
页面绘制:这一块说是简单,但是java的图形控件我使的很少,现在基本上也不用,有机会就随便学学!如果非要谈设计,如下图所示:
项目架构:其实就是两个Main方法,也就是两个主线程之间的交互。一个是ChatServer服务端,一个是ChatClient客户端,代码我暂时没有做更详细的分层,结构见下图
package com.mychat;import java.nio.channels.SocketChannel;/** * 在线用户类 * @author ccq * */public class User { private String userName; private SocketChannel socketChannel; public User(String userName, SocketChannel socketChannel) { this.userName = userName; this.socketChannel = socketChannel; } public String getUserName() { return userName; } public void setUserName(String userName) { this.userName = userName; } public SocketChannel getSocketChannel() { return socketChannel; } public void setSocketChannel(SocketChannel socketChannel) { this.socketChannel = socketChannel; }}2、消息类(Message)
package com.mychat;import net.sf.json.JSONObject;/** * 消息类 * * @author ccq * */public class Message { private String command; // 命令 private String status; // 状态 private String content; // 内容 private String fromUserName; private String toUserName; public Message() {} public Message(String command, String status, String content, String fromUserName, String toUserName) { super(); this.command = command; this.status = status; this.content = content; this.fromUserName = fromUserName; this.toUserName = toUserName; } public String getCommand() { return command; } public void setCommand(String command) { this.command = command; } public String getStatus() { return status; } public void setStatus(String status) { this.status = status; } public String getContent() { return content; } public void setContent(String content) { this.content = content; } public String getFromUserName() { return fromUserName; } public void setFromUserName(String fromUserName) { this.fromUserName = fromUserName; } public String getToUserName() { return toUserName; } public void setToUserName(String toUserName) { this.toUserName = toUserName; } public static void main(String[] args) { Message msg = new Message("login","success","你好", "张三", "李四"); JSONObject object = JSONObject.fromObject(msg); Message bean = (Message) JSONObject.toBean(object, Message.class); System.out.println(bean.getCommand()); } @Override public String toString() { return "Message [command=" + command + ", status=" + status + ", content=" + content + ", fromUserName=" + fromUserName + ", toUserName=" + toUserName + "]"; } }
package com.mychat;import java.text.SimpleDateFormat;import java.util.Date;/** * 日期工具类 * @author ccq * */public class DateUtils { private static final String PATTERN = "yyyy-MM-dd HH:mm:ss"; public static String getCurrentDate(Date date) { SimpleDateFormat simpleDateFormat = new SimpleDateFormat(PATTERN); return simpleDateFormat.format(date); }}4、服务端类(ChatServer)
/** * 初始化页面组件 */ private void initComponents() { /******************用户信息和连接配置*********************/ settingPanel = new JPanel(); settingPanel.setBorder(new TitledBorder("服务器配置")); settingPanel.setLayout(new GridLayout(1, 6, 5, 10)); /******************配置信息设置*********************/ ipField = new JTextField("127.0.0.1"); portField = new JTextField("9090"); ipLabel = new JLabel("服务端ip:"); portLabel = new JLabel("服务端端口:"); startServerBtn = new JButton(START_SERVER); stopServerBtn = new JButton(STOP_SERVER); /******************将组件添加到配置中*********************/ settingPanel.add(ipLabel); settingPanel.add(ipField); settingPanel.add(portLabel); settingPanel.add(portField); settingPanel.add(startServerBtn); settingPanel.add(stopServerBtn); /******************左边的在线用户*********************/ listModel = new DefaultListModel(2)按钮的监听事件(); friendList = new JList (listModel); JScrollPane leftScroll = new JScrollPane(friendList); leftScroll.setBorder(new TitledBorder("在线用户")); /******************右边的历史消息显示和发送消息*********************/ chatPanel = new JPanel(new BorderLayout()); contentPanel = new JPanel(new BorderLayout()); chatContentField = new JTextField(); sendBtn = new JButton(SEND); clearContentBtn = new JButton(CLEAR_CONTENT); contentPanel.add(chatContentField, BorderLayout.CENTER); JPanel btnPanel = new JPanel(new GridLayout(1, 2, 5, 5)); btnPanel.add(sendBtn); btnPanel.add(clearContentBtn); contentPanel.add(chatContentField, BorderLayout.CENTER); contentPanel.add(btnPanel, BorderLayout.EAST); contentPanel.setBorder(new TitledBorder("发送消息")); historyRecordArea = new JTextArea(); historyRecordArea.setForeground(Color.blue); historyRecordArea.setEditable(false); chatPanel.add(historyRecordArea,BorderLayout.CENTER); chatPanel.add(contentPanel, BorderLayout.SOUTH); JScrollPane rightScroll = new JScrollPane(chatPanel); rightScroll.setBorder(new TitledBorder("消息显示区")); /******************设置左右显示定位*********************/ JSplitPane centerSplit = new JSplitPane(JSplitPane.HORIZONTAL_SPLIT, leftScroll,rightScroll); centerSplit.setDividerLocation(100); /******************设置主体定位*********************/ getContentPane().add(settingPanel,BorderLayout.NORTH); getContentPane().add(centerSplit,BorderLayout.CENTER); /******************初始化按钮和文本框状态*********************/ initBtnAndTextConnect(); /******************设置窗体大小和居中显示*********************/ this.setTitle("服务器"); this.setSize(800, 500); this.setLocationRelativeTo(this.getOwner()); this.setDefaultCloseOperation(JFrame.EXIT_ON_CLOSE); this.setVisible(true); }
/** * 设置按钮的监听事件 */ private void setupListener() { startServerBtn.addActionListener(this); stopServerBtn.addActionListener(this); sendBtn.addActionListener(this); clearContentBtn.addActionListener(this); // 发送消息的文本框回车事件 chatContentField.addActionListener(this); }(3)对于监听事件的处理
// 用于监听按钮的点击事件 @Override public void actionPerformed(ActionEvent e) { String actionCommand = e.getActionCommand(); if (START_SERVER.equals(actionCommand)) { try { // 服务启动 String serverIp = ipField.getText(); String portStr = portField.getText(); if (StringUtils.isEmpty(serverIp) || StringUtils.isEmpty(portStr)) { JOptionPane.showMessageDialog(this, "请输入服务器ip和端口号!"); return; } // 初始化连接信息 initConnection(InetAddress.getLocalHost(), Integer.parseInt(portStr)); connect(); setTitle("服务器 - " + hostAddress.getHostAddress()); } catch (NumberFormatException e1) { JOptionPane.showMessageDialog(this, "端口输入异常,请输入数字(如:8080)", "错误", JOptionPane.ERROR_MESSAGE); //e1.printStackTrace(); } catch (Exception e1) { JOptionPane.showMessageDialog(this, "服务启动失败!" + e1.getMessage(), "错误", JOptionPane.ERROR_MESSAGE); //e1.printStackTrace(); } } else if (STOP_SERVER.equals(actionCommand)) { // 关闭服务器 this.shutdown(serverThread); } else if (SEND.equals(actionCommand) || e.getSource() == chatContentField) { //发送按钮和文本框回车事件 String message = chatContentField.getText(); chatContentField.setText(""); if (message == null || message.equals("")) { JOptionPane.showMessageDialog(this, "消息不能为空!", "错误", JOptionPane.ERROR_MESSAGE); return; } String toUserName = this.getSelectedUser(); if(toUserName.equals(ALL_USER_COMMAND)) { historyRecordArea.append(formatMessage("对所有人说:" + message)); }else { historyRecordArea.append(formatMessage("对 " + toUserName + " 说:" + message)); } try { this.sendMsgToUser(toUserName,message); } catch (IOException e1) { JOptionPane.showMessageDialog(this, "消息发送失败" + e1.getMessage(), "错误", JOptionPane.ERROR_MESSAGE); //e1.printStackTrace(); } }else if(CLEAR_CONTENT.equals(actionCommand)) { // 清空历史聊天记录 historyRecordArea.setText(""); } }(4)服务端线程(处理客户端发来消息)
// 服务器线程,用与监听事件 class ServerThread extends Thread{ @Override public void run() { try { while(selector.select()>0) { SetselectedKeys = selector.selectedKeys(); Iterator iterator = selectedKeys.iterator(); while(iterator.hasNext()) { SelectionKey key = iterator.next(); if(!key.isValid()) { continue; } if(key.isAcceptable()) { accept(key); } else if(key.isReadable()) { read(key); } iterator.remove(); } } } catch (IOException e) { e.printStackTrace(); } } }
// 接受事件 private void accept(SelectionKey key) throws IOException { ServerSocketChannel serverSocketChannel = (ServerSocketChannel) key.channel(); SocketChannel socketChannel = serverSocketChannel.accept(); //System.out.println(socketChannel.getRemoteAddress()); //Socket socket = socketChannel.socket(); historyRecordArea.append(formatMessage(socketChannel.getRemoteAddress() + " 连接请求")); socketChannel.configureBlocking(false); socketChannel.register(this.selector, SelectionKey.OP_READ); key.interestOps(SelectionKey.OP_ACCEPT); }
// 读取事件 private void read(SelectionKey key) throws IOException { SocketChannel socketChannel = (SocketChannel) key.channel(); this.readBuffer.clear(); int len = 0; try { len = socketChannel.read(this.readBuffer); } catch (IOException e) { // 远程强制关闭通道,取消选择键并关闭通道 closeClient(key,socketChannel); return; } if(len == -1) { // 客户端通道调用close进行关闭,取消选择键并关闭通道 closeClient(key,socketChannel); return; } String msg = new String(this.readBuffer.array(),0,len); Message message = (Message) JSONObject.toBean(JSONObject.fromObject(msg), Message.class); String command = message.getCommand(); String fromUserName = message.getFromUserName(); String content = message.getContent(); String toUserName = message.getToUserName(); Message returnMsg = new Message(); Message toAllMsg = new Message(); // 业务逻辑处理 switch(command) { case LOGIN_COMMAND: System.out.println(formatMessage("用户 :" + fromUserName + "请求登录...")); String password = PropertyFactory.getProperty(fromUserName); if(password == null) { System.out.println(formatMessage("用户:" + fromUserName + "不存在")); returnMsg.setContent("用户不存在"); returnMsg.setStatus("MSG_PWD_ERROR"); historyRecordArea.append(formatMessage("用户 :" + fromUserName+ "不存在!")); }else if(password.equals(content)) { if(!userMap.containsKey(fromUserName)) { System.out.println(formatMessage("用户:"+ fromUserName +"登录成功!")); User user = new User(fromUserName, socketChannel); userMap.put(fromUserName, user); returnMsg.setContent("用户:"+ fromUserName +"登录成功!"); returnMsg.setStatus("MSG_SUCCESS"); returnMsg.setFromUserName(fromUserName); listModel.addElement(fromUserName); historyRecordArea.append(formatMessage(fromUserName+ " 成功上线!")); }else { System.out.println(formatMessage("该帐号已经登录")); returnMsg.setContent("用户:"+ fromUserName +"已经登录!"); returnMsg.setStatus("MSG_REPEAT"); historyRecordArea.append(formatMessage(fromUserName+ " 重复登陆,失败!")); } }else { returnMsg.setContent("密码错误"); returnMsg.setStatus("MSG_PWD_ERROR"); historyRecordArea.append(formatMessage("用户 :" + fromUserName+ "密码错误!")); } returnMsg.setCommand(LOGIN_COMMAND); //发送登录结果 sendMessage(socketChannel, returnMsg); break; case CHAT_COMMAND: historyRecordArea.append(formatMessage("用户:"+ fromUserName + "发消息给用户:" + toUserName + ", 内容是:" + content)); returnMsg.setCommand(CHAT_COMMAND); // 群聊 if(StringUtils.isNotEmpty(toUserName) && ALL_USER_COMMAND.equals(toUserName)) { returnMsg.setFromUserName(fromUserName); returnMsg.setToUserName(toUserName); returnMsg.setStatus("MSG_SUCCESS"); returnMsg.setContent(content); sendAllUserMessage(returnMsg); break; } // 私聊 if(userMap.containsKey(fromUserName) && userMap.containsKey(toUserName) && StringUtils.isNotEmpty(content)) { SocketChannel sc = userMap.get(toUserName).getSocketChannel(); returnMsg.setFromUserName(fromUserName); returnMsg.setToUserName(toUserName); returnMsg.setStatus("MSG_SUCCESS"); returnMsg.setContent(content); sendMessage(sc, returnMsg); }else { returnMsg.setFromUserName(fromUserName); returnMsg.setToUserName(toUserName); returnMsg.setStatus("MSG_ERROR"); returnMsg.setContent("消息发送失败!"); sendMessage(socketChannel, returnMsg); } break; case ONLINE_USERLIST_COMMAND: // 通知所有人上线消息 toAllMsg.setCommand(ONLINE_USER_COMMAND); toAllMsg.setFromUserName(fromUserName); sendAllMessage(toAllMsg); } }(5)、显示消息的模板方法
// 消息记录显示模板 public String formatMessage(String connect) { return String.format(DateUtils.getCurrentDate(new Date())+ SEPARATOR + "%s\n", connect); }(6)、发送消息方法
// 发送消息 private void sendMessage(SocketChannel socketChannel, Message returnMsg) throws IOException { JSONObject msg = JSONObject.fromObject(returnMsg); if(socketChannel != null && msg != null) { byte[] val = msg.toString().getBytes(); socketChannel.write(ByteBuffer.wrap(val)); } } // 用户获取在线用户列表,同时将他上线的消息通知到所有的客户端 public void sendAllMessage(Message message) throws IOException { Message toFromUserMsg = new Message(); StringBuffer onlineUserName = new StringBuffer(); // 通知所有人 他上线了 Set5、客户端类(CharClient)> entrySet = userMap.entrySet(); for(Entry e : entrySet) { if(!e.getKey().equals(message.getFromUserName())) { JSONObject msg = JSONObject.fromObject(message); byte[] val = msg.toString().getBytes(); e.getValue().getSocketChannel().write(ByteBuffer.wrap(val)); onlineUserName.append(e.getKey()).append("#"); } } // 返回在线用户列表 if(onlineUserName.length() > 1) { String userNames = onlineUserName.substring(0, onlineUserName.length()-1); System.out.println(userNames); toFromUserMsg.setContent(userNames); toFromUserMsg.setCommand(ONLINE_USERLIST_COMMAND); JSONObject msg = JSONObject.fromObject(toFromUserMsg); byte[] val = msg.toString().getBytes(); userMap.get(message.getFromUserName()).getSocketChannel().write(ByteBuffer.wrap(val)); } } // 发送给所有在线用户消息 public void sendAllUserMessage(Message message) throws IOException { Set > entrySet = userMap.entrySet(); for(Entry e : entrySet) { // 群聊不发给自己 if(StringUtils.isNotEmpty(message.getFromUserName()) && e.getKey().equals(message.getFromUserName())) { continue; } JSONObject msg = JSONObject.fromObject(message); byte[] val = msg.toString().getBytes(); e.getValue().getSocketChannel().write(ByteBuffer.wrap(val)); } } // 服务器 聊天发送 private void sendMsgToUser(String toUserName, String content) throws IOException { Message message = new Message(); message.setCommand(CHAT_COMMAND); message.setContent(content); message.setStatus("MSG_SUCCESS"); message.setFromUserName("CCQ服务器"); message.setToUserName(toUserName); if(toUserName.equals(ALL_USER_COMMAND)) { // 群发 sendAllUserMessage(message); }else { // 单发 sendMessage(userMap.get(toUserName).getSocketChannel(), message); } }
// 连接服务器 public void connect() { try { this.selector = Selector.open(); socketChannel = SocketChannel.open(); boolean connect = socketChannel.connect(new InetSocketAddress(this.hostAddress, this.port)); socketChannel.configureBlocking(false); System.out.println("connect = "+connect); socketChannel.register(selector, SelectionKey.OP_READ); historyRecordArea.append(formatMessage("本地连接参数:" + socketChannel.getLocalAddress())); historyRecordArea.append(formatMessage("您已经成功连接服务器 ip:" + hostAddress + " 端口:"+port)); } catch (ClosedChannelException e) { historyRecordArea.append(formatMessage("====服务器连接失败!===" + e.getMessage())); e.printStackTrace(); } catch (IOException e) { historyRecordArea.append(formatMessage("服务器连接失败!" + e.getMessage())); e.printStackTrace(); } ClientThread clientThread = new ClientThread(); // 设置客户端线程为守护线程 clientThread.setDaemon(true); clientThread.start(); }(2)客户端线程(接受服务端发来的数据包进行处理逻辑)
// 客户端线程,用于监听事件 class ClientThread extends Thread{ @Override public void run() { try { while(selector.select()>0) { SetselectedKeys = selector.selectedKeys(); Iterator iterator = selectedKeys.iterator(); while(iterator.hasNext()) { SelectionKey key = iterator.next(); if(key.isReadable()) { read(key); } iterator.remove(); } } } catch (IOException e) { e.printStackTrace(); } } } // 读事件 private void read(SelectionKey key) throws IOException { SocketChannel socketChannel = (SocketChannel) key.channel(); this.readBuffer.clear(); int len; try { len = socketChannel.read(this.readBuffer); } catch (IOException e) { key.cancel(); socketChannel.close(); return; } System.out.println("收到字符串长度 len = " + len); if(len == -1) { key.channel().close(); key.cancel(); return; } String msg = new String(this.readBuffer.array(),0,len); Message message = (Message) JSONObject.toBean(JSONObject.fromObject(msg), Message.class); String command = message.getCommand(); String fromUserName = message.getFromUserName(); String content = message.getContent(); String toUserName = message.getToUserName(); String status = message.getStatus(); // 逻辑处理 switch(command) { case LOGIN_COMMAND: if("MSG_SUCCESS".equals(status)) { this.userName = fromUserName; showBtnAndTextConnectSuccess(); historyRecordArea.append(formatMessage("您已成功上线!")); // 获取在线用户列表 this.findOnlineList(); }else if("MSG_PWD_ERROR".equals(status)){ JOptionPane.showMessageDialog(this, content, "错误", JOptionPane.ERROR_MESSAGE); this.selector.close(); this.socketChannel.close(); historyRecordArea.append(formatMessage("登录失败," + content)); } else if("MSG_REPEAT".equals(status)){ JOptionPane.showMessageDialog(this, content, "错误", JOptionPane.ERROR_MESSAGE); this.selector.close(); this.socketChannel.close(); historyRecordArea.append(formatMessage("登录失败," + content)); } break; case CHAT_COMMAND: if("MSG_SUCCESS".equals(status)) { if(StringUtils.isNotEmpty(toUserName) && ALL_USER_COMMAND.equals(toUserName)) { historyRecordArea.append(formatMessage(fromUserName + "对所有人说:" + content)); }else { historyRecordArea.append(formatMessage(fromUserName + "说:" + content)); } }else { historyRecordArea.setDisabledTextColor(Color.BLACK); historyRecordArea.append(formatMessage("失败消息###发送给"+ toUserName+ " :" + content)); } break; case ONLINE_USER_COMMAND: historyRecordArea.append(formatMessage(fromUserName + "上线了!")); listModel.addElement(fromUserName); break; case OFFLINE_USE_COMMAND: historyRecordArea.append(formatMessage(fromUserName + "下线了!")); listModel.removeElement(fromUserName); case ONLINE_USERLIST_COMMAND: String[] userNames = content.split("#"); System.out.println(userNames.length + "==============在线人数================"); for(int i=0; i