?主要添加了私聊功能
1服務器類定義與成員變量
public class ChatServer {int port = 6666;// 定義服務器端口號為 6666ServerSocket ss;// 定義一個 ServerSocket 對象用于監聽客戶端連接//List<Socket> clientSockets = new ArrayList<>();// 定義一個列表用于存儲已連接的客戶端 Socket 對象List<Socket> clientSockets = new CopyOnWriteArrayList<>();List<String> clientNames = new ArrayList<>(10);//迭代時會復制整個底層數組,因此在遍歷過程中其他線程對集合的修改不會影響當前遍歷,// 有效避免了 ConcurrentModificationException 異常。
}
- 端口號:服務器的端口號被定義為
6666
,客戶端需要知道這個端口號才能與服務器建立連接。 - ServerSocket 對象:
ServerSocket
是 Java 提供的一種用于服務器端編程的類,它負責監聽特定端口上的客戶端連接請求。一旦有客戶端連接,就會創建一個新的Socket
對象來處理與該客戶端之間的通信。 - 客戶端套接字列表:
clientSockets
使用了CopyOnWriteArrayList
來存儲與客戶端建立連接的Socket
對象。這種數據結構在遍歷時會復制整個底層數組,因此在遍歷過程中其他線程對集合的修改不會影響當前遍歷,有效避免了ConcurrentModificationException
異常。 - 客戶端名稱列表:
clientNames
用于存儲每個連接客戶端的名稱,方便在消息中區分不同的客戶端。
2初始化服務器
public void initServer() {// 初始化服務器的方法try {ss = new ServerSocket(port);// 創建 ServerSocket 對象并綁定到指定端口System.out.println("服務器啟動,等待客戶端連接...");} catch (IOException e) {throw new RuntimeException(e);}}
- ?在服務器初始化方法
initServer
中,通過調用ServerSocket
的構造函數并傳入端口號,創建了一個ServerSocket
對象,綁定了服務器到指定端口,使服務器能夠監聽該端口上的客戶端連接請求。一旦初始化成功,就會打印出 "服務器啟動,等待客戶端連接..." 的提示信息,告知服務器已正常啟動并處于監聽狀態。
3監聽客戶端連接
public void listenerConnection() {// 監聽客戶端連接的方法,返回連接的 Socket 對象new Thread(()->{while(true){try {Socket socket = ss.accept();// 調用 accept() 方法等待客戶端連接clientNames.add("Hello");//clientSockets.add(socket);synchronized (clientSockets) {// 同步操作確保線程安全clientSockets.add(socket);// 將連接的客戶端 Socket 對象添加到列表中}System.out.println("客戶端已連接:" + socket.getInetAddress().getHostAddress());// 輸出客戶端連接成功提示信息及客戶端 IP 地址} catch (IOException e) {throw new RuntimeException(e);}}}).start();}
listenerConnection
方法創建了一個新線程,用于不斷地監聽客戶端的連接請求。在循環中,調用ss.accept()
方法阻塞等待客戶端的連接。一旦有客戶端連接,就會創建一個新的Socket
對象,并將其添加到clientSockets
列表中。同時,向clientNames
列表中添加一個默認客戶端名稱 "Hello",后續可以根據實際情況更新該名稱。每當有新的客戶端連接成功,就會打印出客戶端的 IP 地址,表明該客戶端已成功連接到服務器。
4讀取客戶端消息
public void readMsg(List<Socket> clientSockets, JTextArea msgShow) {// 讀取客戶端消息的方法//System.out.println("clientSockets size: " + clientSockets.size()); // 檢查列表大小synchronized (clientSockets) {// 對客戶端列表進行同步操作Thread tt = new Thread(() -> {// 創建一個線程用于讀取并處理客戶端消息//System.out.println("開始讀取客戶端發送的消息");while (true) {// 無限循環持續讀取消息InputStream is;// 定義輸入流對象用于讀取客戶端消息Socket socket = null;try {Thread.sleep(50);} catch (InterruptedException e) {throw new RuntimeException(e);}for (Socket cSocket : clientSockets) {// 遍歷每個客戶端 Socket//System.out.println("循環每個socket");socket = cSocket;if(socket == null){continue;}try {is = socket.getInputStream();// 獲取客戶端 Socket 對象的輸入流} catch (IOException e) {throw new RuntimeException(e);}try {int idLen = is.read();// 讀取消息中發送方名稱長度的字節if(idLen == 0){continue;}byte[] id = new byte[idLen];// 根據讀取的長度創建字節數組存儲發送方名稱is.read(id);// 讀取發送方名稱字節數組int si = clientSockets.indexOf(socket);clientNames.set(si,new String(id));int msgLen = is.read();// 讀取消息內容長度的字節if(msgLen == 0){continue;}byte[] msg = new byte[msgLen];// 根據讀取的長度創建字節數組存儲消息內容is.read(msg);// 讀取消息內容字節數組String clientmsg = new String(msg);//先判斷@有沒有int start = clientmsg.indexOf('@');if(start != -1){int end = clientmsg.indexOf(' ');String friendID = clientmsg.substring(start+1,end);String message = clientmsg.substring(end);System.out.println(new String(id) + "發送給" + friendID + " 的一條私聊消息:" + message);// 將字節數組轉換為字符串并輸出消息內容msgShow.append(new String(id) + "發送給" + friendID + " 的一條私聊消息:" + message + "\n");for (Socket clientSocket : clientSockets) {// 遍歷所有已連接的客戶端 Socket 對象if (clientSocket == socket) {// 如果是當前發送消息的客戶端continue;}int s = clientSockets.indexOf(clientSocket);String name = clientNames.get(s);if(name.equals(friendID)){OutputStream os = null;// 定義輸出流對象用于向其他客戶端發送消息os = clientSocket.getOutputStream();// 獲取客戶端 Socket 對象的輸出流os.write(id.length);// 發送發送方名稱長度os.write(id);// 發送發送方名稱字節數組message += "(這是一條私聊消息)";os.write(message.getBytes().length);// 發送消息內容長度os.write(message.getBytes());// 發送消息內容字節數組os.flush();// 刷新輸出流確保數據發送完成break;}}}else {System.out.println(new String(id) + "發送的消息:" + new String(msg));// 將字節數組轉換為字符串并輸出消息內容msgShow.append(new String(id) + "說:" + new String(msg) + "\n");// 轉發信息給所有其他客戶端for (Socket clientSocket : clientSockets) {// 遍歷所有已連接的客戶端 Socket 對象if (clientSocket == socket) {// 如果是當前發送消息的客戶端continue;}OutputStream os = null;// 定義輸出流對象用于向其他客戶端發送消息os = clientSocket.getOutputStream();// 獲取客戶端 Socket 對象的輸出流os.write(id.length);// 發送發送方名稱長度os.write(id);// 發送發送方名稱字節數組os.write(msg.length);// 發送消息內容長度os.write(msg);// 發送消息內容字節數組os.flush();// 刷新輸出流確保數據發送完成}}} catch (IOException e) {throw new RuntimeException(e);}}}});tt.start();}}
readMsg
方法的核心功能是讀取客戶端發送的消息,并根據消息類型(普通消息或私聊消息)進行相應的處理和轉發。- 首先,創建一個新線程
tt
,在無限循環中依次檢查clientSockets
列表中的每個Socket
對象。對于每個客戶端Socket
,通過socket.getInputStream()
獲取輸入流,從而讀取客戶端發送的數據。 - 客戶端發送的數據格式預先約定為:首先是一個字節表示發送方名稱的長度,然后是發送方名稱對應的字節數組;接下來是一個字節表示消息內容長度,最后是消息內容對應的字節數組。按照這種格式,先讀取發送方名稱長度
idLen
,根據該長度創建字節數組id
讀取發送方名稱,再讀取消息內容長度msgLen
,創建字節數組msg
讀取消息內容。 - 隨后,將讀取到的發送方名稱更新到對應的
clientNames
列表位置。接下來對消息內容進行判斷,如果消息內容中包含 "@" 符號,則認為這是一條私聊消息。通過解析消息內容,獲取私聊目標的名稱friendID
和實際消息內容message
,然后在clientSockets
中查找對應的私聊目標客戶端Socket
,并將私聊消息發送給該目標客戶端。 - 如果消息內容中不包含 "@" 符號,則認為這是一條普通消息,直接將消息轉發給除發送方外的所有其他客戶端。
- 無論是普通消息還是私聊消息,都通過輸出流
OutputStream
將消息發送給目標客戶端。發送時,同樣按照約定的數據格式,先發送發送方名稱的長度和名稱字節數組,再發送消息內容的長度和內容字節數組,并調用os.flush()
刷新輸出流確保數據發送完成。
5服務器啟動
public void start() {// 啟動服務器的方法initServer();// 調用初始化服務器的方法//new Thread(()->{//startSend();// 啟動服務端從控制臺向所有客戶端發送消息的線程//}).start();ChatUI ui = new ChatUI("服務端", clientSockets);ui.setVisible(true); // 確保 UI 可見listenerConnection();// 調用監聽客戶端連接的方法readMsg(clientSockets,ui.msgShow);// 調用讀取消息的方法}
- 在
start
方法中,首先調用initServer
方法初始化服務器,然后創建一個ChatUI
對象作為服務器端的用戶界面(假設存在ChatUI
類,用于展示聊天內容等信息),使用戶界面可見。接著調用listenerConnection
方法啟動監聽客戶端連接的線程,最后調用readMsg
方法啟動讀取消息的線程,并將讀取到的消息顯示在用戶界面中。
6主方法
public static void main(String[] args) {ChatServer server = new ChatServer();// 創建 ChatServer 對象server.start();// 調用啟動服務器的方法}
main
方法作為程序的入口,創建了一個ChatServer
對象,并調用其start
方法啟動服務器。?
7其他模塊代碼不變
public class Client {Socket socket;// 定義 Socket 對象用于與服務器建立連接String ip;// 定義服務器 IP 地址int port;// 定義服務器端口號InputStream in;// 定義輸入流對象用于讀取服務器發送的消息OutputStream out;// 定義輸出流對象用于向服務器發送消息public Client(String ip, int port) {// 構造方法,初始化客戶端 IP 地址和端口號this.ip = ip;this.port = port;}public void connectServer(String userName) {// 連接服務器的方法try {socket = new Socket(ip, port);// 創建 Socket 對象連接到指定 IPin = socket.getInputStream();// 獲取 Socket 對象的輸入流用于讀取消息out = socket.getOutputStream();// 獲取 Socket 對象的輸出流用于發送消息try {out.write(userName.length());out.write(userName.getBytes());out.flush();} catch (IOException e) {throw new RuntimeException(e);}System.out.println("連接服務器成功");} catch (IOException e) {throw new RuntimeException(e);}}public void readMsg(JTextArea msgShow) {// 讀取服務器發送的消息的方法new Thread(() -> {// 創建一個線程用于讀取并處理服務器消息try {System.out.println("開始讀取消息");while (true) { // 無限循環持續讀取消息int senderNameLength = in.read();// 讀取發送方名稱長度的字節byte[] senderNameBytes = new byte[senderNameLength];// 根據讀取的長度創建字節數組存儲發送方名稱in.read(senderNameBytes);// 讀取發送方名稱字節數組int msgLength = in.read();// 讀取消息內容長度的字節byte[] msgBytes = new byte[msgLength];// 根據讀取的長度創建字節數組存儲消息內容in.read(msgBytes);// 讀取消息內容字節數組System.out.println(new String(senderNameBytes) + "發送的消息:" + new String(msgBytes));// 將字節數組轉換為字符串并輸出消息內容msgShow.append(new String(senderNameBytes) +"說:" + new String(msgBytes) + "\n");}} catch (IOException e) {throw new RuntimeException(e);}}).start();}/*public void startSend() {// 從控制臺向服務器發送消息的方法new Thread(() -> {// 創建一個線程用于讀取控制臺輸入并發送消息try {BufferedReader reader = new BufferedReader(new InputStreamReader(System.in));// 創建緩沖讀取器讀取控制臺輸入System.out.println("請輸入用戶名:");String userName = reader.readLine();// 讀取一行控制臺輸入作為用戶名System.out.println("請輸入消息(按回車發送):");while (true) {// 無限循環持續讀取并發送消息String msg = reader.readLine();// 讀取一行控制臺輸入作為消息內容if (msg != null && !msg.isEmpty()) {// 如果輸入的消息不為空out.write(userName.getBytes().length);// 發送用戶名長度out.write(userName.getBytes());// 發送用戶名字節數組out.write(msg.getBytes().length);// 發送消息內容長度out.write(msg.getBytes());// 發送消息內容字節數組out.flush();// 刷新輸出流確保數據發送完成}}} catch (IOException e) {throw new RuntimeException(e);}}).start();}*/public void startClient() {// 啟動客戶端的方法String userName = JOptionPane.showInputDialog("請輸入用戶名:");connectServer(userName);// 調用連接服務器的方法ChatUI ui = new ChatUI(userName, out);readMsg(ui.msgShow);// 調用讀取消息的方法//startSend();// 調用發送消息的方法try {Thread.sleep(500);} catch (InterruptedException e) {throw new RuntimeException(e);}new Thread() {public void run() {while (true) {try {out.write(0);out.flush();Thread.sleep(500);} catch (IOException e) {throw new RuntimeException(e);} catch (InterruptedException e) {throw new RuntimeException(e);}}}}.start();}public static void main(String[] args) {Client client = new Client("127.0.0.1", 6666);// 創建 Client 對象,連接到本地主機的 6666 端口client.startClient();// 調用啟動客戶端的方法}
}
public class ChatUI extends JFrame {public JTextArea msgShow = new JTextArea();// 顯示消息的文本區域public ChatUI(String title, List<Socket> clientSockets) {// 服務器端構造方法super(title);// 設置窗口標題setSize(500, 500);// 設置窗口大小setDefaultCloseOperation(JFrame.EXIT_ON_CLOSE);// 設置關閉操作JScrollPane scrollPane = new JScrollPane(msgShow);// 創建滾動面板包括消息顯示區域scrollPane.setPreferredSize(new Dimension(0, 350));add(scrollPane, BorderLayout.NORTH);// 添加到窗口北部// 創建消息輸入面板及組件JPanel msgInput = new JPanel();JTextArea msg = new JTextArea();JScrollPane scrollPane1 = new JScrollPane(msg);scrollPane1.setPreferredSize(new Dimension(480, 80));msgInput.add(scrollPane1);JButton send = new JButton("發送");msgInput.add(send);msgInput.setPreferredSize(new Dimension(0, 120));add(msgInput, BorderLayout.SOUTH);// 添加到窗口南部setVisible(true);ChatListener cl = new ChatListener();// 創建事件監聽器send.addActionListener(cl);// 為發送按鈕添加監聽器cl.showMsg = msgShow;// 傳遞消息顯示組件cl.msgInput = msg;cl.userName = title;cl.clientSockets = clientSockets;}public ChatUI(String title, OutputStream out) {// 客戶端構造方法super(title);setSize(500, 500);setDefaultCloseOperation(JFrame.EXIT_ON_CLOSE);JScrollPane scrollPane = new JScrollPane(msgShow);scrollPane.setPreferredSize(new Dimension(0, 350));add(scrollPane, BorderLayout.NORTH);JPanel msgInput = new JPanel();JTextArea msg = new JTextArea();JScrollPane scrollPane1 = new JScrollPane(msg);scrollPane1.setPreferredSize(new Dimension(480, 80));msgInput.add(scrollPane1);JButton send = new JButton("發送");msgInput.add(send);msgInput.setPreferredSize(new Dimension(0, 120));add(msgInput, BorderLayout.SOUTH);setVisible(true);clientListener cl = new clientListener();send.addActionListener(cl);cl.showMsg = msgShow;cl.msgInput = msg;cl.userName = title;cl.out = out;}
}
public class ChatListener implements ActionListener {public List<Socket> clientSockets;// 客戶端 Socket 列表JTextArea showMsg;// 消息顯示區域JTextArea msgInput;// 消息輸入區域String userName;// 用戶名OutputStream out;// 輸出流public void actionPerformed(ActionEvent e) {// 處理發送按鈕點擊事件String text = msgInput.getText();// 獲取輸入的消息文本showMsg.append(userName + ": " + text + "\n");// 在顯示區域追加消息for (Socket cSocket : clientSockets) {// 遍歷所有客戶端Socket socket = cSocket;try {out = socket.getOutputStream();// 獲取客戶端輸出流out.write(userName.getBytes().length);// 發送用戶名長度out.write(userName.getBytes());// 發送用戶名out.write(text.getBytes().length);// 發送消息內容長度out.write(text.getBytes());// 發送消息內容out.flush();// 刷新輸出流} catch (IOException ex) {throw new RuntimeException(ex);}}}}
public class clientListener implements ActionListener {JTextArea showMsg;// 消息顯示區域JTextArea msgInput;// 消息輸入區域String userName;// 用戶名OutputStream out;// 輸出流public void actionPerformed(ActionEvent e) {// 處理發送按鈕點擊String text = msgInput.getText();// 獲取輸入消息showMsg.append(userName + ": " + text + "\n");// 顯示消息try {out.write(userName.getBytes().length);// 發送用戶名長度out.write(userName.getBytes());// 發送用戶名out.write(text.getBytes().length);// 發送消息長度out.write(text.getBytes());// 發送消息內容out.flush();// 刷新輸出流//msgInput.setText(""); // 清空輸入框} catch (IOException ex) {throw new RuntimeException(ex);}}
}