基于NIO非阻塞的java聊天demo(支持单聊和群聊)

来源:互联网 发布:在ipad上开淘宝店 编辑:程序博客网 时间:2024/05/16 02:34

1、聊天demo介绍


首先,你需要了解什么是缓存区(buffer)、通道(channel)、选择器(selector)、TCP协议、java组件Swing(这玩意我以为不会,需要用到什么百度查查就ok)。

其次对java网络编程socket有过简单的应用,起码有过认识,这样在看demo可能会理解更快!

最后,说到这里,先放最后的效果图吧,页面设计一般,请亲喷。





如上图所示,分别是服务端页面和客户端页面,其中服务端分为“服务器配置”、“在线用户列表”、“消息显示区”、“发送消息区”,客户端页面设计差不多,但是在去连接服务端时需要进行用户名和密码的校验,这算是一个基本的功能。


2、项目架构分析


页面绘制:这一块说是简单,但是java的图形控件我使的很少,现在基本上也不用,有机会就随便学学!如果非要谈设计,如下图所示:



项目架构:其实就是两个Main方法,也就是两个主线程之间的交互。一个是ChatServer服务端,一个是ChatClient客户端,代码我暂时没有做更详细的分层,结构见下图



3、功能分析


既然是聊天的demo,功能类似于扣扣吧,简单画图如下:

服务端功能:
(1)提供服务开启和服务关闭
(2)校验用户信息,完成登录检查
(3)接受用户数据包,解析做处理(这就需要有约定的协议)
(4)提供在线用户列表查询
客户端功能:
(1)连接服务端
(2)可以进行登录
(3)查询在线用户列表
(4)选中用户进行消息发送

其中,有很多的异常需要处理,列举以下
(1)服务端服务开启
(2)服务端服务正常关闭和异常关闭
(3)转发给用户聊天信息
(4)客户端正常关闭和异常关闭
(5)客户端登录失败
(6)客户端发送消息失败

4、详细设计与代码实现

1、用户类(User)
用户保存用户名和对应的socketChannel,主要是服务对用户聊天信息进行转发,将信息写到对应用户的通道中
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());}@Overridepublic String toString() {return "Message [command=" + command + ", status=" + status + ", content=" + content + ", fromUserName="+ fromUserName + ", toUserName=" + toUserName + "]";}}

3、日期格式化类(DateUtils)
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)

(1)初始化页面组件(绘制页面)
/** * 初始化页面组件 */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<String>();friendList = new JList<String>(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);}

(2)按钮的监听事件
/** * 设置按钮的监听事件 */private void setupListener() {startServerBtn.addActionListener(this);stopServerBtn.addActionListener(this);sendBtn.addActionListener(this);clearContentBtn.addActionListener(this);// 发送消息的文本框回车事件chatContentField.addActionListener(this);}

(3)对于监听事件的处理
// 用于监听按钮的点击事件@Overridepublic 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{@Overridepublic void run() {try {while(selector.select()>0) {Set<SelectionKey> selectedKeys = selector.selectedKeys();Iterator<SelectionKey> 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();// 通知所有人 他上线了Set<Entry<String, User>> entrySet = userMap.entrySet();for(Entry<String, User> 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<Entry<String, User>> entrySet = userMap.entrySet();for(Entry<String, User> 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);}}

5、客户端类(CharClient)

和服务端差不多,贴一下主要的逻辑代码

(1)连接服务器
// 连接服务器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{@Overridepublic void run() {try {while(selector.select()>0) {Set<SelectionKey> selectedKeys = selector.selectedKeys();Iterator<SelectionKey> 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<userNames.length; i++) {if(!userNames[i].equals(this.userName)) {listModel.addElement(userNames[i]);}}break;case SERVER_STOP:shutdownConnect();}key.interestOps(SelectionKey.OP_READ);}

5、总结

对于还不熟悉的NIO朋友,我建议先去看看基础吧,在最开始的地方我有说重要的点!
从上周开始,经历初始NIO——>熟悉NIO——>简单编写demo,终于完成这个小程序,其实还是挺好玩的,最近对TCP协议挺有兴趣的,多看多学多做吧!
最后再放几张demo聊天截图:













代码下载地址:http://download.csdn.net/download/qq_27717967/10132940

阅读全文
0 0
原创粉丝点击