博客
关于我
强烈建议你试试无所不能的chatGPT,快点击我
基于NIO非阻塞的java聊天demo(支持单聊和群聊)
阅读量:3733 次
发布时间:2019-05-22

本文共 19721 字,大约阅读时间需要 65 分钟。

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());	}	@Override	public 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
(); 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); }
(2)按钮的监听事件
/**	 * 设置按钮的监听事件	 */	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) {					Set
selectedKeys = 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();				// 通知所有人 他上线了		Set
> 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); } }
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{				@Override		public void run() {			try {				while(selector.select()>0) {					Set
selectedKeys = 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

5、总结

对于还不熟悉的NIO朋友,我建议先去看看基础吧,在最开始的地方我有说重要的点!
从上周开始,经历初始NIO——>熟悉NIO——>简单编写demo,终于完成这个小程序,其实还是挺好玩的,最近对TCP协议挺有兴趣的,多看多学多做吧!
最后再放几张demo聊天截图:
代码下载地址:
github代码下载:
你可能感兴趣的文章
搞定Go语言之第三天
查看>>
Docker Compose
查看>>
搞定Go语言之第四天
查看>>
基于GO语言实现书籍管理系统
查看>>
搞定Go语言之第五天
查看>>
搞定Go语言之第六天
查看>>
ETCD实战
查看>>
influxDB时序数据库的使用
查看>>
Grafana使用
查看>>
kail linux暴力破解wifi
查看>>
最简明的设计模式
查看>>
Docker四种网络模式
查看>>
docker安装OSSRS流媒体直播服务器
查看>>
服务发现-Consul
查看>>
NSQ入门
查看>>
Python常用collections模块
查看>>
Gin框架安装的一些坑
查看>>
基于SSM框架的网上购物商城及电商后台管理系统
查看>>
Web应用系统开发——基于ThinkPhp5的商品后台管理系统
查看>>
Java企业级项目实训——基于微信小程序的帮跑腿平台开发
查看>>