我们将分步实现,并提供完整的代码。

核心设计思路
一个典型的 C/S(客户端/服务器)架构的多人聊天室包含两个主要部分:
-
服务器端:
- 监听一个固定端口,等待客户端的连接请求。
- 当一个客户端连接成功后,服务器会为该客户端创建一个独立的
Socket和一个专门的线程(ClientHandler)来处理与该客户端的所有通信。 - 服务器维护一个所有客户端连接的列表(
List<ClientHandler>)。 - 当服务器从某个客户端收到消息时,它会将这条消息广播给列表中的所有其他客户端。
-
客户端:
- 知道服务器的 IP 地址和端口号。
- 连接到服务器。
- 启动两个线程:
- 输入线程: 持续监听用户在控制台的输入,一旦用户输入消息并按下回车,就将消息发送给服务器。
- 输出线程: 持续监听从服务器接收到的消息,一旦收到,就打印在控制台上,让所有用户看到。
第一步:服务器端实现
服务器端的核心是 ServerSocket 和一个处理单个客户端的线程类。

ClientHandler.java - 处理单个客户端的线程
这个类是每个客户端连接到服务器时,服务器为其创建的一个“管家”,它负责接收该客户端的消息,并转发给其他所有人。
import java.io.BufferedReader;
import java.io.IOException;
import java.io.InputStreamReader;
import java.io.PrintWriter;
import java.net.Socket;
import java.util.ArrayList;
import java.util.List;
// 这是一个处理单个客户端连接的线程
public class ClientHandler implements Runnable {
private Socket clientSocket;
private PrintWriter out;
private BufferedReader in;
// 使用静态列表来保存所有客户端处理器,这样它们之间可以共享
private static final List<ClientHandler> clients = new ArrayList<>();
public ClientHandler(Socket socket) {
this.clientSocket = socket;
try {
// 获取输出流,用于向客户端发送消息
out = new PrintWriter(clientSocket.getOutputStream(), true);
// 获取输入流,用于接收客户端的消息
in = new BufferedReader(new InputStreamReader(clientSocket.getInputStream()));
// 将新加入的客户端处理器添加到列表中
clients.add(this);
System.out.println("新客户端连接: " + clientSocket.getInetAddress().getHostAddress());
} catch (IOException e) {
e.printStackTrace();
}
}
@Override
public void run() {
try {
String inputLine;
// 循环读取客户端发送的消息
while ((inputLine = in.readLine()) != null) {
System.out.println("收到来自 " + clientSocket.getInetAddress().getHostAddress() + " 的消息: " + inputLine);
// 将消息广播给所有其他客户端
broadcast(inputLine);
}
} catch (IOException e) {
// 如果客户端断开连接,会抛出异常
System.out.println("客户端 " + clientSocket.getInetAddress().getHostAddress() + " 断开连接。");
} finally {
// 客户端断开连接后,清理资源并从列表中移除
removeClientHandler();
try {
clientSocket.close();
} catch (IOException e) {
e.printStackTrace();
}
}
}
// 广播消息给所有客户端
private void broadcast(String message) {
// 遍历所有客户端处理器
for (ClientHandler client : clients) {
// 向每个客户端发送消息,但不发送给自己(可选,也可以发送给自己)
// if (client != this) {
client.out.println(message);
// }
}
}
// 从客户端列表中移除自己
private void removeClientHandler() {
clients.remove(this);
}
}
ChatServer.java - 主服务器类
这是服务器的入口,负责启动服务器并接受新的客户端连接。
import java.io.IOException;
import java.net.ServerSocket;
import java.net.Socket;
import java.util.concurrent.ExecutorService;
import java.util.concurrent.Executors;
public class ChatServer {
private static final int PORT = 12345; // 服务器监听端口
public static void main(String[] args) {
// 使用线程池来管理客户端连接线程,避免为每个连接创建新线程的开销
ExecutorService threadPool = Executors.newCachedThreadPool();
try (ServerSocket serverSocket = new ServerSocket(PORT)) {
System.out.println("聊天服务器已启动,监听端口: " + PORT);
// 无限循环,持续接受客户端连接
while (true) {
Socket clientSocket = serverSocket.accept(); // 阻塞,等待客户端连接
System.out.println("已接受来自 " + clientSocket.getInetAddress().getHostAddress() + " 的连接请求。");
// 为每个新连接创建一个ClientHandler任务,并提交给线程池执行
threadPool.execute(new ClientHandler(clientSocket));
}
} catch (IOException e) {
System.err.println("服务器启动失败或发生IO异常: " + e.getMessage());
e.printStackTrace();
} finally {
// 关闭线程池
threadPool.shutdown();
}
}
}
第二步:客户端实现
客户端相对简单,需要同时处理用户输入和服务器消息的接收。
ChatClient.java
import java.io.BufferedReader;
import java.io.IOException;
import java.io.InputStreamReader;
import java.io.PrintWriter;
import java.net.Socket;
import java.net.UnknownHostException;
import java.util.Scanner;
public class ChatClient {
private static final String SERVER_HOST = "127.0.0.1"; // 服务器IP地址,本地回环
private static final int SERVER_PORT = 12345; // 服务器端口
public static void main(String[] args) {
try (
// 1. 创建Socket连接到服务器
Socket socket = new Socket(SERVER_HOST, SERVER_PORT);
// 2. 获取输出流,用于向服务器发送消息
PrintWriter out = new PrintWriter(socket.getOutputStream(), true);
// 3. 获取输入流,用于接收服务器的消息
BufferedReader in = new BufferedReader(new InputStreamReader(socket.getInputStream()))
) {
System.out.println("成功连接到聊天服务器!");
// 4. 创建一个线程来专门监听服务器发送过来的消息
Thread receiveThread = new Thread(new Runnable() {
@Override
public void run() {
String serverMessage;
try {
// 循环读取服务器发送的消息
while ((serverMessage = in.readLine()) != null) {
System.out.println(serverMessage);
}
} catch (IOException e) {
// 如果服务器关闭或断开连接,会收到null
System.out.println("与服务器断开连接。");
}
}
});
receiveThread.start(); // 启动接收线程
// 5. 使用主线程来读取用户在控制台的输入
Scanner scanner = new Scanner(System.in);
System.out.print("请输入你的昵称: ");
String nickname = scanner.nextLine();
out.println(nickname + " 加入了聊天室。"); // 发送加入消息
String userInput;
while (true) {
System.out.print("> "); // 提示符
userInput = scanner.nextLine();
if ("exit".equalsIgnoreCase(userInput)) {
out.println(nickname + " 离开了聊天室。"); // 发送离开消息
break; // 用户输入exit,则退出循环
}
// 将用户输入的消息发送给服务器
out.println(nickname + ": " + userInput);
}
} catch (UnknownHostException e) {
System.err.println("找不到服务器: " + e.getMessage());
} catch (IOException e) {
System.err.println("无法连接到服务器: " + e.getMessage());
}
}
}
如何运行
-
编译代码: 将所有
.java文件放在同一个目录下,然后使用javac编译:
(图片来源网络,侵删)javac *.java
-
启动服务器: 在一个终端窗口中运行服务器:
java ChatServer
你会看到输出:
聊天服务器已启动,监听端口: 12345 -
启动客户端: 打开两个或更多新的终端窗口,分别运行客户端:
java ChatClient
在每个客户端中输入你的昵称,然后就可以开始聊天了。
运行效果示例
终端1 (服务器):
聊天服务器已启动,监听端口: 12345
已接受来自 127.0.0.1 的连接请求。
新客户端连接: 127.0.0.1
已接受来自 127.0.0.1 的连接请求。
新客户端连接: 127.0.0.1
收到来自 127.0.0.1 的消息: Alice: 大家好!
收到来自 127.0.0.1 的消息: Bob: 你好,Alice!
终端2 (客户端1, Alice):
成功连接到聊天服务器!
请输入你的昵称: Alice
> 大家好!
Bob: 你好,Alice!
终端3 (客户端2, Bob):
成功连接到聊天服务器!
请输入你的昵称: Bob
Alice 加入了聊天室。
Alice: 大家好!
> 你好,Alice!
进阶与优化
- GUI 图形界面: 目前的版本是基于控制台的,你可以使用 Java Swing 或 JavaFX 来创建图形界面,让聊天体验更友好。
- 私聊功能: 可以在消息中添加特殊格式,
@Bob 你好,服务器解析后只将消息发送给名为 "Bob" 的客户端。 - 用户列表: 服务器可以维护一个在线用户列表,并在有用户加入或离开时广播给所有客户端。
- 处理异常和心跳包: 目前的代码在客户端突然断开时(如直接关闭窗口),服务器可能无法立即感知,可以引入心跳机制,客户端定期向服务器发送“我还活着”的信号,如果服务器长时间没收到,就认为客户端已断开并清理资源。
- 使用 NIO (New I/O): 对于高并发的场景,传统的 BIO (Blocking I/O) 模式(每个连接一个线程)效率不高,可以使用 Java NIO 的 Selector 机制,用一个线程或少量线程来管理成千上万的连接,性能更高。
