java局域网聊天室小项目架构思路
项目需求
创建一个局域网聊天系统,要求:用户在登录界面登录后进入聊天窗口界面,能实现多用户同时在线聊天,并且用户之间可以进行私聊
项目用到的技术栈
- java网络编程
- java多线程
- java面向对象编程
- javaGUI技术(Swing)
第一步:创建项目并构建客户端登录界面与聊天窗口界面
创建项目
创建ChatRoom模块用于存放整个项目,其下创建Chat-Client与Chat-Server两个模块分别用于客户端与服务端的构建
登陆界面
登陆界面要有昵称输入框,用户可输入昵称,输入框下方有“登录”和“取消”两个按钮,两个按钮要绑定事件监听器,
登录按钮监测输入框中是否有文字,是则点击后关闭登录窗口,否则提示输入框不能为空,
取消按钮一旦点击则关闭登录窗口
利用通义千问来辅助完成登录界面
登录界面代码(初期)
import javax.swing.*;
import java.awt.*;
import java.awt.event.*;public class ChatLoginUI extends JFrame implements ActionListener {// 定义界面组件private JLabel nicknameLabel;private JTextField nicknameField;private JButton loginButton;private JButton cancelButton;// 构造方法:初始化界面public ChatLoginUI() {// 设置窗口标题setTitle("聊天室登录界面");// 设置窗口大小setSize(400, 200);// 设置窗口关闭操作(关闭窗口时退出程序)setDefaultCloseOperation(JFrame.EXIT_ON_CLOSE);// 设置窗口居中显示setLocationRelativeTo(null);// 创建面板用于放置组件JPanel panel = new JPanel();panel.setLayout(new FlowLayout());// 创建昵称标签nicknameLabel = new JLabel("昵称:");panel.add(nicknameLabel);// 创建昵称输入框nicknameField = new JTextField(20);panel.add(nicknameField);// 创建登录按钮loginButton = new JButton("登录");loginButton.addActionListener(this); // 绑定事件监听panel.add(loginButton);// 创建取消按钮cancelButton = new JButton("取消");cancelButton.addActionListener(this); // 绑定事件监听panel.add(cancelButton);// 将面板添加到窗口add(panel);// 设置窗口可见setVisible(true);}// 按钮点击事件处理@Overridepublic void actionPerformed(ActionEvent e) {if (e.getSource() == loginButton) {// 获取输入框内容String nickname = nicknameField.getText().trim();// 验证昵称是否为空if (nickname.isEmpty()) {JOptionPane.showMessageDialog(this, "昵称不能为空!", "错误", JOptionPane.ERROR_MESSAGE);} else {// 显示昵称(示例:打印到控制台)System.out.println("登录成功,昵称:" + nickname);// 这里可以添加跳转到聊天室的逻辑dispose(); // 关闭登录窗口}} else if (e.getSource() == cancelButton) {// 取消按钮点击事件:关闭窗口dispose();}}
}
聊天窗口界面
聊天窗口界面左侧为聊天区,右侧实时展示当前登录用户,下方为聊天输入框和发送按钮,当输入文字后,点击发送按钮或者按下回车键即可发送消息到聊天区
借助通义千问来辅助完成
聊天窗口界面(初期)
import javax.swing.*;
import java.awt.*;
import java.awt.event.ActionEvent;
import java.awt.event.ActionListener;public class ChatRoom extends JFrame {private JTextArea chatTextArea;private JTextField messageField;private JButton sendButton;private JList<String> userList;private DefaultListModel<String> userListModel;public ChatRoom() {setTitle("Chat Room");setSize(800, 600);setDefaultCloseOperation(JFrame.EXIT_ON_CLOSE);setLocationRelativeTo(null);// 创建聊天区域chatTextArea = new JTextArea();chatTextArea.setEditable(false);JScrollPane chatScrollPane = new JScrollPane(chatTextArea);// 创建消息输入框和发送按钮JPanel inputPanel = new JPanel(new BorderLayout());messageField = new JTextField();sendButton = new JButton("Send");inputPanel.add(messageField, BorderLayout.CENTER);inputPanel.add(sendButton, BorderLayout.EAST);// 创建用户列表userListModel = new DefaultListModel<>();userList = new JList<>(userListModel);JScrollPane userScrollPane = new JScrollPane(userList);// 设置布局JSplitPane splitPane = new JSplitPane(JSplitPane.HORIZONTAL_SPLIT, chatScrollPane, userScrollPane);splitPane.setDividerLocation(600);getContentPane().add(splitPane, BorderLayout.CENTER);getContentPane().add(inputPanel, BorderLayout.SOUTH);// 添加发送按钮事件监听器sendButton.addActionListener(new ActionListener() {@Overridepublic void actionPerformed(ActionEvent e) {sendMessage();}});// 按Enter键发送消息messageField.addActionListener(new ActionListener() {@Overridepublic void actionPerformed(ActionEvent e) {sendMessage();}});}private void sendMessage() {String message = messageField.getText().trim();if (!message.isEmpty()) {chatTextArea.append("You: " + message + "\n");messageField.setText("");}}// 更新用户列表的方法public void updateUserList(String[] users) {userListModel.clear();for (String user : users) {userListModel.addElement(user);}}
}
登录窗口与聊天窗口正常启动,第一步完成
第二步:构建局域网聊天室服务端
——先构建好服务器模块,再依据服务端逻辑构建客户端,因为服务端逻辑较为清晰简洁——
1.创建服务器启动端
- 由于聊天需要稳定传输,所以采用TCP通信方式
- 打印启动日志表确认服务端启动成功
- 循环监听并捕获客户端链接请求,为每个请求启动一个线程来处理(由于局域网通信人数少,并不会产生问题)
服务器启动端代码(初期)
package ServerTest;import java.io.IOException;
import java.net.ServerSocket;
import java.net.Socket;
import java.util.HashMap;
import java.util.Map;public class Server {public static void main(String[] args) {//创建服务器启动端try {ServerSocket server = new ServerSocket(ConstantServer.SERVER_PORT);//打印启动日志System.out.println("服务器启动成功...");//循环监听客户端连接while (true) {Socket socket = server.accept();//启动一个线程处理客户端请求new ServerReaderThread(socket).start();}} catch (Exception e) {e.printStackTrace();}}
}
2.创建服务器处理端
- 由于所有请求处理是同时进行的,所以采用多线程
- 用户发送来的消息包括1.登陆消息,2.群发消息,3.私聊消息
- 为每种消息设置标记进行区分,不同消息不同处理
- 客户端下线处理
由于需要保存用户信息,所以应该在服务器启动端设置一个唯一数组用于存储在线用户昵称与在线用户管道,由于具有映射关系所以采用Map数组
public static final Map<Socket,String> userMap = new HashMap<>();
服务器处理端代码
package ServerTest;import java.io.DataInputStream;
import java.io.DataOutputStream;
import java.io.IOException;
import java.net.Socket;
import java.time.LocalDateTime;
import java.time.format.DateTimeFormatter;
import java.util.Collection;
import java.util.Map;public class ServerReaderThread extends Thread{private Socket socket;public ServerReaderThread(Socket socket){this.socket = socket;}@Overridepublic void run(){//开发服务端实现方法//1.获取客户端输入流try {DataInputStream dis = new DataInputStream(socket.getInputStream());//客户端的信息包括1.登录信息,2.群发消息,3.私聊消息while(true){int type = dis.readInt();switch (type) {case ConstantServer.LOGIN://登录信息//获取昵称,并且更新当前用户列表String nickname = dis.readUTF();Server.userMap.put(socket, nickname);updateUserList();break;case ConstantServer.GROUP_MESSAGE://群发消息String msg = dis.readUTF();String name = Server.userMap.get(socket);//设置群发消息方法groupMessage(msg,name);break;case ConstantServer.PRIVATE_MESSAGE://私聊消息String toName = dis.readUTF();String toMsg = dis.readUTF();privateMessage(toName,toMsg);}}} catch (Exception e) {System.out.println("客户端下线:"+socket.getInetAddress().getHostAddress());Server.userMap.remove(socket);updateUserList();}}private void privateMessage(String toName, String toMsg) {//将私聊信息先进行封装,再发送给指定客户端的socket管道StringBuilder sb = new StringBuilder();String name = Server.userMap.get(socket);LocalDateTime now = LocalDateTime.now();DateTimeFormatter df = DateTimeFormatter.ofPattern("yyyy-MM-dd HH:mm:ss EEE a");sb.append(name).append(" ").append(df.format(now)).append("\r\n").append(toMsg).append("\r\n");//如果对方不在线,则向发送消息的客户端发送一个提示信息if(!Server.userMap.containsValue(toName)){try {DataOutputStream dos = new DataOutputStream(socket.getOutputStream());dos.writeInt(ConstantServer.PRIVATE_MESSAGE);dos.writeUTF("----对方不在线,请稍后再试----");return;} catch (Exception e) {e.printStackTrace();}}for(Socket socket:Server.userMap.keySet()){try {DataOutputStream dos = new DataOutputStream(socket.getOutputStream());if(Server.userMap.get(socket).equals(toName)){dos.writeInt(ConstantServer.PRIVATE_MESSAGE);dos.writeUTF(sb.toString());dos.flush();break;}} catch (Exception e) {e.printStackTrace();}}}private void groupMessage(String msg,String name) {//将群发消息转发给所有在线客户端Socket管道//封装消息StringBuilder sb = new StringBuilder();LocalDateTime now = LocalDateTime.now();DateTimeFormatter df = DateTimeFormatter.ofPattern("yyyy-MM-dd HH:mm:ss EEE a");sb.append(name).append(" ").append(df.format(now)).append("\r\n").append(msg).append("\r\n");//发送消息for (Socket socket:Server.userMap.keySet()) {try {DataOutputStream dos = new DataOutputStream(socket.getOutputStream());dos.writeInt(ConstantServer.GROUP_MESSAGE);dos.writeUTF(sb.toString());dos.flush();//及时刷新} catch (Exception e) {e.printStackTrace();}}}private void updateUserList() {//更新在线用户列表//拿到所有在线客户端用户名称,将这些数据转发给全部在线客户端Socket管道Collection<String> users = Server.userMap.values();for (Socket socket:Server.userMap.keySet()) {try {DataOutputStream dos = new DataOutputStream(socket.getOutputStream());dos.writeInt(ConstantServer.LOGIN);dos.writeInt(users.size());for(String user:users){dos.writeUTF(user);}dos.flush();} catch (Exception e) {e.printStackTrace();}}}}
3.服务端各种常量
由于IP与端口等可变,所以将所有可变常量存放在一个类中,便于修改
package ServerTest;public class ConstantServer {public static final String SERVER_IP = "127.0.0.1";public static final int SERVER_PORT = 8888;public static final int LOGIN = 1;public static final int GROUP_MESSAGE = 2;public static final int PRIVATE_MESSAGE = 3;
}
第三步:连接客户端与服务端
1.完善登录界面功能
- 实现登录逻辑,将登录信息发送给服务端
- 实现切换窗口逻辑
登陆界面代码(完善后)
@Overridepublic void actionPerformed(ActionEvent e) {if (e.getSource() == loginButton) {// 获取输入框内容String nickname = nicknameField.getText().trim();// 验证昵称是否为空if (nickname.isEmpty()) {JOptionPane.showMessageDialog(this, "昵称不能为空!", "错误", JOptionPane.ERROR_MESSAGE);} else {// 实现登录逻辑try {loginroom(nickname);} catch (Exception ex) {ex.printStackTrace();}// 这里可以添加跳转到聊天室的逻辑new ChatRoomUI(nickname,socket);dispose(); // 关闭登录窗口}} else if (e.getSource() == cancelButton) {// 取消按钮点击事件:关闭窗口dispose();}}private void loginroom(String nickname) throws Exception {//立即将登录消息发送给服务器socket = new Socket(ConstantClient.SERVER_IP,ConstantClient.SERVER_PORT);DataOutputStream dos = new DataOutputStream(socket.getOutputStream());dos.writeInt(ConstantClient.LOGIN);dos.writeUTF(nickname);dos.flush();}
2.完善聊天窗口功能
- 创建一个客户端接受信息类,用于接收服务端传回的信息,1.更新用户列表的消息,2.群发的消息,3.私聊的消息
- 将接收到的信息反映到用户界面上
客户端接受信息类
package UI;import java.io.DataInputStream;
import java.io.DataOutputStream;
import java.io.IOException;
import java.net.Socket;public class ClentReaderThread extends Thread{private Socket socket;private ChatRoomUI chatRoomUI;//每个管道对应一个界面private DataInputStream dis;public ClentReaderThread(Socket socket,ChatRoomUI chatRoomUI){this.socket = socket;this.chatRoomUI = chatRoomUI;}@Overridepublic void run(){try {dis = new DataInputStream(socket.getInputStream());//接受服务端发送的消息1.更新人数的消息,2.群发的消息,3.私聊的消息while(true){int type = dis.readInt();switch (type) {case ConstantClient.UPDATE_USERLIST://更新用户列表updateUserList();break;case ConstantClient.GROUP_MESSAGE://群发消息gotoshowmsg();break;case ConstantClient.PRIVATE_MESSAGE://私聊消息gotoshowPrivatemsg();break;}}} catch (Exception e) {e.printStackTrace();}}//接收私聊消息方法private void gotoshowPrivatemsg() throws Exception {String msg = dis.readUTF();chatRoomUI.showPrivateMsg(msg);}//接收群发消息方法private void gotoshowmsg() throws Exception {String msg = dis.readUTF();chatRoomUI.showMsg(msg);}//接收更新的用户列表方法private void updateUserList() throws Exception {int count = dis.readInt();String[] users = new String[count];for (int i = 0; i < count; i++) {users[i] = dis.readUTF();}chatRoomUI.updateUserList(users);}
}
聊天界面代码(完善后)
新增双击私发消息的功能
package UI;import javax.swing.*;
import java.awt.*;
import java.awt.event.ActionEvent;
import java.awt.event.ActionListener;
import java.awt.event.MouseAdapter;
import java.awt.event.MouseEvent;
import java.io.DataOutputStream;
import java.net.Socket;public class ChatRoomUI extends JFrame {private JTextArea chatTextArea;private JTextField messageField;private JButton sendButton;private JList<String> userList;private DefaultListModel<String> userListModel;private Socket socket;private String nickname;public ChatRoomUI() {initFrame();setVisible(true);}public void initFrame(){setSize(800, 600);setDefaultCloseOperation(JFrame.EXIT_ON_CLOSE);setLocationRelativeTo(null);// 创建聊天区域chatTextArea = new JTextArea();chatTextArea.setEditable(false);JScrollPane chatScrollPane = new JScrollPane(chatTextArea);// 创建消息输入框和发送按钮JPanel inputPanel = new JPanel(new BorderLayout());messageField = new JTextField();sendButton = new JButton("Send");inputPanel.add(messageField, BorderLayout.CENTER);inputPanel.add(sendButton, BorderLayout.EAST);// 创建用户列表userListModel = new DefaultListModel<>();userList = new JList<>(userListModel);JScrollPane userScrollPane = new JScrollPane(userList);// 设置布局JSplitPane splitPane = new JSplitPane(JSplitPane.HORIZONTAL_SPLIT, chatScrollPane, userScrollPane);splitPane.setDividerLocation(600);getContentPane().add(splitPane, BorderLayout.CENTER);getContentPane().add(inputPanel, BorderLayout.SOUTH);// 添加发送按钮事件监听器sendButton.addActionListener(new ActionListener() {@Overridepublic void actionPerformed(ActionEvent e) {sendMessage();}});// 按Enter键发送消息messageField.addActionListener(new ActionListener() {@Overridepublic void actionPerformed(ActionEvent e) {sendMessage();}});// 添加用户列表双击事件监听器,用于私聊userList.addMouseListener(new MouseAdapter() {@Overridepublic void mouseClicked(MouseEvent e) {// 判断是否为双击事件if (e.getClickCount() == 2) {// 获取选中的用户索引int selectedIndex = userList.getSelectedIndex();if (selectedIndex != -1) {// 获取选中的用户名String selectedUser = userListModel.getElementAt(selectedIndex);// 不能给自己发私聊消息if (!selectedUser.equals(nickname)) {// 弹出输入框,获取私聊内容String privateMessage = JOptionPane.showInputDialog(ChatRoomUI.this,"发送私聊消息给 " + selectedUser + ":", "私聊", JOptionPane.PLAIN_MESSAGE);// 发送私聊消息if (privateMessage != null && !privateMessage.trim().isEmpty()) {sendPrivateMessage(selectedUser, privateMessage.trim());}} else {// 提示不能给自己发消息JOptionPane.showMessageDialog(ChatRoomUI.this, "不能给自己发送私聊消息", "提示", JOptionPane.WARNING_MESSAGE);}}}}});}public ChatRoomUI(String nickname, Socket socket) {this();setTitle(nickname+"的聊天窗口");this.nickname = nickname;this.socket = socket;new ClentReaderThread(socket,this).start();}//发送群发消息到服务器方法private void sendMessage() {String message = messageField.getText().trim();if (!message.isEmpty()) {//将消息发送给服务器try {DataOutputStream dos = new DataOutputStream(socket.getOutputStream());dos.writeInt(ConstantClient.GROUP_MESSAGE);dos.writeUTF(message);dos.flush();messageField.setText("");} catch (Exception e) {e.printStackTrace();}}}//发送私聊消息到服务器的方法private void sendPrivateMessage(String toUser, String message) {try {DataOutputStream dos = new DataOutputStream(socket.getOutputStream());dos.writeInt(ConstantClient.PRIVATE_MESSAGE);dos.writeUTF(toUser);dos.writeUTF(message);dos.flush();chatTextArea.append("[私聊给 " + toUser + "] " + message + "\n");} catch (Exception e) {e.printStackTrace();}}//展示更新用户列表方法public void updateUserList(String[] users) {userListModel.clear();for (String user : users) {userListModel.addElement(user);}}//展示群发消息方法public void showMsg(String msg) {if (!msg.isEmpty()) {chatTextArea.append(msg + "\n");messageField.setText("");}}//展示私聊消息方法public void showPrivateMsg(String msg) {if (!msg.isEmpty()) {chatTextArea.append("[私聊] " + msg + "\n");}}
}
项目地址https://gitcode.com/2401_88685396/myfirstgitcodeofjava