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 + "]";
}
}
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)对于监听事件的处理
// 用于监听按钮的点击事件
@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<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{
@Override
public 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聊天截图: