- 服务器端:监听客户端连接,并将一个客户端发送的消息广播给所有已连接的客户端。
- 客户端:连接到服务器,通过控制台输入消息并发送给服务器,同时也能在控制台看到其他客户端发送的消息。
核心概念
在开始编码前,我们先理解几个核心概念:

- Socket (套接字):网络通信的端点,可以把它想象成一个电话,你通过它来拨号(连接)和通话(发送/接收数据)。
- ServerSocket:服务器端的“总机”,它负责在指定的端口上监听客户端的连接请求,当有客户端请求连接时,它会“接起电话”,并为这个客户端创建一个新的
Socket来进行一对一的通信。 - IP 地址:网络中设备的唯一标识,
0.0.1(本机地址)。 - 端口号:设备上应用程序的标识,确保数据被发送到正确的程序,0 到 1023 是系统保留端口,我们应该使用 1024 以上的端口。
- 输入/输出流:
Socket对象包含InputStream和OutputStream,用于数据的读写,在 Java 中,我们通常会包装成更高级的BufferedReader和PrintWriter来方便地处理文本数据。
服务器端代码
服务器端的主要任务是:
- 在一个固定端口启动
ServerSocket。 - 进入一个无限循环,不断等待新的客户端连接。
- 当有新客户端连接时,为其创建一个新的线程,专门负责处理与该客户端的通信(接收消息并广播)。
- 维护一个所有客户端线程的列表,以便广播消息。
文件名:ChatServer.java
import java.io.*;
import java.net.*;
import java.util.*;
import java.util.concurrent.CopyOnWriteArrayList;
public class ChatServer {
// 使用线程安全的 List 来存储所有客户端的处理线程
private static final List<ClientHandler> clients = new CopyOnWriteArrayList<>();
public static void main(String[] args) {
// 服务器监听的端口号
int port = 12345;
try (ServerSocket serverSocket = new ServerSocket(port)) {
System.out.println("聊天服务器已启动,等待客户端连接...");
// 主线程负责不断接受新的客户端连接
while (true) {
Socket clientSocket = serverSocket.accept();
System.out.println("新客户端已连接: " + clientSocket.getInetAddress().getHostAddress());
// 为每个客户端创建一个新的处理线程
ClientHandler clientHandler = new ClientHandler(clientSocket);
clients.add(clientHandler);
// 启动客户端处理线程
new Thread(clientHandler).start();
}
} catch (IOException e) {
System.err.println("服务器启动或运行时出错: " + e.getMessage());
e.printStackTrace();
}
}
// 广播消息给所有客户端
public static void broadcast(String message, ClientHandler excludeClient) {
for (ClientHandler client : clients) {
if (client != excludeClient) {
client.sendMessage(message);
}
}
}
// 内部类:用于处理单个客户端的通信
static class ClientHandler implements Runnable {
private Socket socket;
private PrintWriter out;
private BufferedReader in;
private String username;
public ClientHandler(Socket socket) {
this.socket = socket;
}
@Override
public void run() {
try {
// 获取输入输出流
out = new PrintWriter(socket.getOutputStream(), true);
in = new BufferedReader(new InputStreamReader(socket.getInputStream()));
// 提示用户输入用户名
out.println("请输入您的用户名:");
username = in.readLine();
System.out.println(username + " 已加入聊天室。");
// 广播新用户加入的消息
broadcast("[系统] " + username + " 加入了聊天室。", this);
// 读取客户端发送的消息
String inputLine;
while ((inputLine = in.readLine()) != null) {
if ("exit".equalsIgnoreCase(inputLine)) {
break; // 客户端退出
}
// 广播消息给其他客户端
broadcast(username + ": " + inputLine, this);
}
} catch (IOException e) {
// 如果客户端异常断开,这里会捕获到异常
System.out.println("与客户端 " + username + " 的连接中断。");
} finally {
// 客户端断开连接后的清理工作
try {
if (socket != null && !socket.isClosed()) {
socket.close();
}
} catch (IOException e) {
e.printStackTrace();
}
// 从客户端列表中移除
clients.remove(this);
// 广播用户离开的消息
if (username != null) {
System.out.println(username + " 已离开聊天室。");
broadcast("[系统] " + username + " 离开了聊天室。", this);
}
}
}
// 向此客户端发送消息
public void sendMessage(String message) {
out.println(message);
}
}
}
客户端代码
客户端的主要任务是:
- 创建一个
Socket对象,连接到服务器的 IP 地址和端口号。 - 创建一个线程来持续监听服务器发来的消息,并打印到控制台。
- 主线程负责从控制台读取用户输入,并将输入的内容发送给服务器。
文件名:ChatClient.java

import java.io.*;
import java.net.*;
public class ChatClient {
private static final String SERVER_ADDRESS = "127.0.0.1"; // 服务器地址
private static final int SERVER_PORT = 12345; // 服务器端口
public static void main(String[] args) {
try (
// 1. 创建 Socket 连接到服务器
Socket socket = new Socket(SERVER_ADDRESS, SERVER_PORT);
// 2. 获取输入输出流
PrintWriter out = new PrintWriter(socket.getOutputStream(), true);
BufferedReader in = new BufferedReader(new InputStreamReader(socket.getInputStream()));
// 3. 获取控制台输入流
BufferedReader stdIn = new BufferedReader(new InputStreamReader(System.in))
) {
System.out.println("已连接到服务器。");
// 4. 创建一个新线程来接收服务器消息
Thread receiverThread = new Thread(() -> {
try {
String serverMessage;
while ((serverMessage = in.readLine()) != null) {
System.out.println(serverMessage);
}
} catch (IOException e) {
System.out.println("与服务器的连接已断开。");
}
});
receiverThread.start();
// 5. 主线程负责从控制台读取用户输入并发送给服务器
String userInput;
while (true) {
userInput = stdIn.readLine();
if ("exit".equalsIgnoreCase(userInput)) {
break; // 输入 exit 退出
}
out.println(userInput);
}
} catch (UnknownHostException e) {
System.err.println("无法找到服务器: " + e.getMessage());
} catch (IOException e) {
System.err.println("I/O Error: " + e.getMessage());
}
System.out.println("客户端已关闭。");
}
}
如何运行和测试
-
保存文件:将上面的两段代码分别保存为
ChatServer.java和ChatClient.java,并放在同一个目录下。 -
编译代码:打开终端或命令提示符,进入文件所在目录,运行以下命令编译 Java 文件:
javac ChatServer.java ChatClient.java
-
启动服务器:在同一个终端中,先运行服务器程序:
java ChatServer
你会看到输出:
聊天服务器已启动,等待客户端连接... -
启动客户端:再打开一个新的终端(不要关闭服务器的终端),在新的终端中运行客户端程序:
java ChatClient
你会看到:
已连接到服务器。 请输入您的用户名:输入一个用户名,
Alice,然后按回车。 -
开始聊天:
- 在第一个客户端(Alice)的终端,输入任何消息,"大家好!",然后按回车。
- 在第二个客户端(Bob)的终端,你也会看到 "Alice: 大家好!"。
- 再打开第三个终端,启动另一个
java ChatClient,输入用户名Bob。 - 当 Alice 或 Bob 说话时,另一方和服务器控制台都能看到消息。
-
退出:
- 在任意一个客户端的终端输入
exit并按回车,该客户端会退出。 - 其他客户端和服务器会收到该用户离开的系统消息。
- 服务器会一直运行,等待新的客户端连接。
- 在任意一个客户端的终端输入
代码讲解与改进点
服务器端 (ChatServer.java)
CopyOnWriteArrayList<ClientHandler>:这是一个线程安全的List,在多线程环境下(服务器主线程添加,客户端线程删除),使用它比ArrayList更安全,可以避免ConcurrentModificationException。ServerSocket serverSocket = new ServerSocket(port):在指定端口创建服务器套接字。serverSocket.accept():这是一个阻塞方法,程序会在这里暂停,直到有新的客户端连接,一旦有连接,它会返回一个代表该客户端连接的Socket对象。new Thread(clientHandler).start():为每个客户端创建一个独立的线程,这是处理多个客户端连接的关键,这样服务器就不会因为处理一个客户端而阻塞,从而无法响应其他客户端。broadcast()方法:遍历所有客户端列表,向除了发送者之外的所有客户端发送消息。
客户端 (ChatClient.java)
new Socket(SERVER_ADDRESS, SERVER_PORT):创建一个Socket并尝试连接到指定的服务器地址和端口,这是一个阻塞方法,连接成功后才会继续执行。- 为什么需要接收线程?:网络通信是异步的,我们不能在主线程中一边等待用户输入(
stdIn.readLine()),一边又等待服务器消息(in.readLine()),因为readLine()都是阻塞的,我们创建一个专门的线程来监听服务器消息,而主线程则专注于处理用户输入。 out.println():将用户输入的一行文本发送到服务器。in.readLine():读取服务器传来的一行文本。
潜在的改进方向
- 图形用户界面:目前是基于命令行的,可以扩展为使用 Java Swing 或 JavaFX 来创建有聊天窗口、输入框和按钮的 GUI 应用。
- 私聊功能:可以在消息前加上特定格式(如
@用户名 消息内容),服务器解析后只将消息发送给指定的用户。 - 文件传输:可以通过定义新的消息协议(以
FILE:开头)来实现文件传输功能。 - 更健壮的错误处理:增加对网络中断、异常输入等情况的更细致处理。
- 使用 NIO (New I/O):对于更高性能、更高并发的场景,可以使用 Java NIO 中的
Selector和Channel来实现非阻塞 I/O,这能更高效地管理大量连接。
