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