可先下载程序运行看效果再开发
本项目的目标是构建一个基于 TCP 的多用户实时群聊系统,由两个独立运行的 Java 程序组成:
服务端程序:长期运行,监听客户端连接,管理所有在线用户,并将任意用户发送的消息广播给其他所有用户。
客户端程序:用户启动后连接服务器,输入用户名,随后可发送消息;同时实时接收并显示所有其他用户的消息。

代码结构清晰:采用三层架构(表现层 / 业务逻辑层 / 数据访问层)组织代码。
高内聚低耦合:各层之间通过接口交互,禁止直接依赖具体实现类。
线程安全:服务端管理客户端列表时必须保证并发安全。
资源管理:所有 I/O 流和 Socket 必须在使用完毕后正确关闭。
整个系统分为两个独立程序,每个程序内部均采用三层架构:
Server 程序
├── 表现层(Presentation Layer)
│ └── ServerApp.java // 启动入口
├── 业务逻辑层(Service Layer)
│ ├── IServerService.java // 服务接口
│ └── ChatServerService.java // 服务实现
└── 数据访问层(Data Access Layer)
├── IServerDataAccess.java // 数据访问接口
└── ServerSocketDataAccess.java // 基于 Socket 的实现
└── ClientHandler.java // 单个客户端连接处理器(属于数据层辅助组件)
Client 程序
├── 表现层(Presentation Layer)
│ └── ClientApp.java // 启动入口
├── 业务逻辑层(Service Layer)
│ ├── IClientService.java // 客户端服务接口
│ └── ChatClientService.java // 客户端服务实现
└── 数据访问层(Data Access Layer)
├── IClientDataAccess.java // 客户端数据访问接口
└── ClientSocketDataAccess.java // 基于 Socket 的实现关键原则:上层只依赖下层的接口,不依赖具体实现。例如
ChatServerService依赖IServerDataAccess,而非ServerSocketDataAccess
为确保客户端与服务端协同工作,双方需遵守以下通信协议:
先定义所有接口(IServerService, IClientService 等)
实现服务端数据层(ServerSocketDataAccess, ClientHandler)
实现服务端业务层(ChatServerService)
编写服务端启动类(ServerApp),测试单客户端连接
实现客户端各层,测试消息收发
启动多个客户端,验证广播功能
启动服务端,无客户端连接 → 服务端正常等待
启动一个客户端,输入用户名 → 收到欢迎消息
该客户端发送消息 → 自己看不到自己的消息(符合广播规则)
启动第二个客户端 → 两人互相可见对方消息
关闭一个客户端 → 服务端日志显示“用户已离开”,另一客户端不受影响
创建一个名为 ChatServer 的项目
业务逻辑层:IServerService
package com.lusifer.chat.server.service;
import com.lusifer.chat.server.handle.ClientHandler;
/**
* 定义服务器服务的核心功能。
* 该接口提供了启动服务器、广播消息、注册客户端以及注销客户端的方法。
*/
public interface IServerService {
/**
* 启动服务器并监听指定端口。
*
* @param port 服务器监听的端口号
*/
void start(int port);
/**
* 向所有已连接的客户端广播一条消息。
*
* @param message 要广播的消息内容
* @param senderUsername 发送消息的用户名
*/
void broadcast(String message, String senderUsername);
/**
* 注册一个新的客户端处理器到服务器。
*
* @param handler 客户端处理器实例
*/
void registerClient(ClientHandler handler);
/**
* 从服务器中注销一个客户端处理器。
*
* @param handler 要注销的客户端处理器实例
*/
void unregisterClient(ClientHandler handler);
/**
* 服务端主动向所有客户端广播消息。
*
* @param message 要广播的消息内容
*/
void broadcastFromServer(String message);
}数据访问层:IServerDataAccess
package com.lusifer.chat.server.dao;
import com.lusifer.chat.server.service.IServerService;
/**
* 定义服务器数据访问的相关操作。
* 该接口用于启动服务器监听指定端口,并将请求委托给指定的服务处理。
*/
public interface IServerDataAccess {
/**
* 启动服务器监听指定端口,并将接收到的请求交由指定的服务处理。
*
* @param port 服务器监听的端口号,必须是一个有效的端口值(通常为1-65535)。
* @param service 用于处理客户端请求的服务实例,需实现IServerService接口。
*/
void startListening(int port, IServerService service);
}ChatServerService
package com.lusifer.chat.server.service.impl;
import com.lusifer.chat.server.dao.IServerDataAccess;
import com.lusifer.chat.server.dao.impl.ServerSocketDataAccess;
import com.lusifer.chat.server.handle.ClientHandler;
import com.lusifer.chat.server.service.IServerService;
import java.util.concurrent.CopyOnWriteArrayList;
public class ChatServerService implements IServerService {
/**
* 存储所有已连接的客户端处理器的线程安全列表。
*/
private final CopyOnWriteArrayList<ClientHandler> clients = new CopyOnWriteArrayList<>();
@Override
public void start(int port) {
// 创建数据访问对象并启动监听
IServerDataAccess dataAccess = new ServerSocketDataAccess();
dataAccess.startListening(port, this);
}
@Override
public void broadcast(String message, String senderUsername) {
// 遍历所有客户端,向非发送者发送消息
for (ClientHandler client : clients) {
if (!client
.getUsername()
.equals(senderUsername)) {
client.sendMessage(message);
}
}
}
@Override
public void registerClient(ClientHandler handler) {
// 将客户端添加到列表中并打印日志
clients.add(handler);
System.out.println("【服务器】用户 [" + handler.getUsername() + "] 已加入。");
}
@Override
public void unregisterClient(ClientHandler handler) {
// 从列表中移除客户端并打印日志
clients.remove(handler);
System.out.println("【服务器】用户 [" + handler.getUsername() + "] 已离开。");
}
@Override
public void broadcastFromServer(String message) {
// 遍历所有客户端,向每个客户端发送消息
for (ClientHandler client : clients) {
client.sendMessage("[服务器]: " + message);
}
}
}ServerSocketDataAccess
package com.lusifer.chat.server.dao.impl;
import com.lusifer.chat.server.dao.IServerDataAccess;
import com.lusifer.chat.server.handle.ClientHandler;
import com.lusifer.chat.server.service.IServerService;
import java.io.IOException;
import java.net.ServerSocket;
import java.net.Socket;
public class ServerSocketDataAccess implements IServerDataAccess {
@Override
public void startListening(int port, IServerService service) {
// 创建 ServerSocket 实例并绑定到指定端口,使用 try-with-resources 确保资源自动关闭
try (ServerSocket serverSocket = new ServerSocket(port)) {
// 输出服务器启动信息
System.out.println("【服务器】已启动,监听端口 " + port + "...");
// 持续监听客户端连接,直到当前线程被中断
while (!Thread
.currentThread()
.isInterrupted()) {
// 接受客户端连接,返回对应的 Socket 实例
Socket clientSocket = serverSocket.accept();
// 为每个客户端连接创建一个 ClientHandler 处理器
ClientHandler handler = new ClientHandler(clientSocket, service);
// 启动新线程处理客户端请求
new Thread(handler).start();
}
} catch (IOException e) {
// 捕获并输出服务器监听失败的错误信息
System.err.println("【服务器】监听失败: " + e.getMessage());
}
}
}ClientHandler
package com.lusifer.chat.server.handle;
import com.lusifer.chat.server.service.IServerService;
import java.io.BufferedReader;
import java.io.IOException;
import java.io.InputStreamReader;
import java.io.PrintWriter;
import java.net.Socket;
/**
* 实现了 Runnable 接口,用于处理单个客户端的连接和通信。
* 该类负责接收客户端的消息、广播消息到其他客户端,并管理客户端的注册与注销。
*/
public class ClientHandler implements Runnable {
private final Socket socket; // 客户端套接字连接
private final PrintWriter out; // 向客户端发送消息的输出流
private final BufferedReader in; // 从客户端读取消息的输入流
private String username; // 客户端的用户名
private final IServerService serverService; // 服务器服务接口,用于注册、注销和广播消息
/**
* 构造函数,初始化客户端处理器。
*
* @param socket 客户端的套接字连接
* @param serverService 服务器服务接口实例
* @throws IOException 如果创建输入/输出流时发生错误
*/
public ClientHandler(Socket socket, IServerService serverService) throws IOException {
this.socket = socket;
this.serverService = serverService;
this.out = new PrintWriter(socket.getOutputStream(), true);
this.in = new BufferedReader(new InputStreamReader(socket.getInputStream()));
}
/**
* 获取客户端的用户名。
*
* @return 客户端的用户名
*/
public String getUsername() {
return username;
}
/**
* 向客户端发送一条消息。
*
* @param message 要发送的消息内容
*/
public void sendMessage(String message) {
out.println(message);
}
/**
* 实现 Runnable 接口的 run 方法,处理客户端的整个生命周期。
* 包括获取用户名、注册到服务器、处理聊天消息以及最终的清理工作。
*/
@Override
public void run() {
try {
// 第一步:获取用户名并注册到服务器
out.println("请输入您的用户名:");
username = in.readLine();
if (username == null || username
.trim()
.isEmpty()) {
username = "匿名用户";
}
// 注册客户端到服务器
serverService.registerClient(this);
out.println("欢迎加入聊天室," + username + "!");
out.println("您可以开始发送消息了...");
// 第二步:持续读取客户端发送的消息并广播给其他客户端
String line;
while ((line = in.readLine()) != null) {
String formattedMessage = "[" + username + "]: " + line;
System.out.println(formattedMessage); // 打印到服务端日志
serverService.broadcast(formattedMessage, username); // 广播消息给所有其他客户端
}
} catch (IOException ignored) {
// 忽略IO异常
} finally {
// 清理资源:注销客户端并关闭套接字
try {
serverService.unregisterClient(this);
socket.close();
} catch (IOException ignored) {
// 忽略关闭套接字时的异常
}
}
}
}ServerApp
package com.lusifer.chat.server;
import com.lusifer.chat.server.service.IServerService;
import com.lusifer.chat.server.service.impl.ChatServerService;
import java.util.Scanner;
public class ServerApp {
public static void main(String[] args) {
IServerService serverService = new ChatServerService();
// 启动一个线程监听服务端输入
new Thread(() -> {
Scanner scanner = new Scanner(System.in);
System.out.println("输入消息并按回车即可广播给所有客户端:");
while (true) {
String input = scanner.nextLine(); // 读取服务端输入
serverService.broadcastFromServer(input); // 广播消息
}
}).start();
serverService.start(12346); // 启动服务
}
}创建一个名为 ChatClient 的项目
业务逻辑层:IClientService
package com.lusifer.chat.client.service;
/**
* 定义了客户端服务的基本功能。
* 该接口提供了连接服务器、接收消息和发送用户消息的方法。
*/
public interface IClientService {
/**
* 连接到指定的主机和端口。
*
* @param host 主机地址,用于建立网络连接。
* @param port 端口号,用于建立网络连接。
* @throws Exception 如果连接过程中发生错误,则抛出异常。
*/
void connect(String host, int port) throws Exception;
/**
* 启动消息接收功能。
* 调用此方法后,客户端将开始监听并处理来自服务器的消息。
*/
void startReceivingMessages();
/**
* 发送用户消息到服务器。
*
* @param message 要发送的用户消息内容。
*/
void sendUserMessage(String message);
}数据访问层:IClientDataAccess
package com.lusifer.chat.client.dao;
import java.io.IOException;
/**
* 定义了客户端数据访问的基本操作。
* 该接口提供了连接、发送消息、接收消息以及关闭连接的功能。
*/
public interface IClientDataAccess {
/**
* 建立与指定主机和端口的连接。
*
* @param host 主机地址,用于建立网络连接。
* @param port 端口号,用于建立网络连接。
* @throws IOException 当连接过程中发生I/O异常时抛出。
*/
void connect(String host, int port) throws IOException;
/**
* 向已建立的连接发送消息。
*
* @param message 要发送的消息内容。
*/
void send(String message);
/**
* 从连接中接收消息。
*
* @return 接收到的消息内容。
*/
String receive();
/**
* 关闭当前的连接。
*/
void close();
}ChatClientService
package com.lusifer.chat.client.service.impl;
import com.lusifer.chat.client.dao.IClientDataAccess;
import com.lusifer.chat.client.service.IClientService;
public class ChatClientService implements IClientService {
/**
* 数据访问接口实例,用于执行底层的网络通信操作。
*/
private final IClientDataAccess dataAccess;
/**
* 构造函数。
*
* @param dataAccess 数据访问接口实例,用于处理连接、发送和接收消息等操作。
*/
public ChatClientService(IClientDataAccess dataAccess) {
this.dataAccess = dataAccess;
}
@Override
public void connect(String host, int port) throws Exception {
dataAccess.connect(host, port);
}
@Override
public void startReceivingMessages() {
// 创建并启动一个新的线程用于接收消息
new Thread(() -> {
// 循环接收消息,直到线程被中断或连接关闭
while (!Thread
.currentThread()
.isInterrupted()) {
String message = dataAccess.receive();
if (message != null) {
System.out.println(message); // 显示所有收到的消息
} else {
break; // 连接关闭时退出循环
}
}
}).start();
}
@Override
public void sendUserMessage(String message) {
dataAccess.send(message);
}
}ClientSocketDataAccess
package com.lusifer.chat.client.dao.impl;
import com.lusifer.chat.client.dao.IClientDataAccess;
import java.io.BufferedReader;
import java.io.IOException;
import java.io.InputStreamReader;
import java.io.PrintWriter;
import java.net.Socket;
public class ClientSocketDataAccess implements IClientDataAccess {
private Socket socket;
private PrintWriter out;
private BufferedReader in;
@Override
public void connect(String host, int port) throws IOException {
socket = new Socket(host, port);
out = new PrintWriter(socket.getOutputStream(), true);
in = new BufferedReader(new InputStreamReader(socket.getInputStream()));
}
@Override
public void send(String message) {
out.println(message);
}
@Override
public String receive() {
try {
return in.readLine();
} catch (IOException e) {
return null;
}
}
@Override
public void close() {
try {
if (in != null) in.close();
if (out != null) out.close();
if (socket != null && !socket.isClosed()) socket.close();
} catch (IOException ignored) {
}
}
}ClientApp
package com.lusifer.chat.client;
import com.lusifer.chat.client.dao.IClientDataAccess;
import com.lusifer.chat.client.dao.impl.ClientSocketDataAccess;
import com.lusifer.chat.client.service.IClientService;
import com.lusifer.chat.client.service.impl.ChatClientService;
import java.util.Scanner;
public class ClientApp {
public static void main(String[] args) {
// 定义服务器主机地址和端口号
final String SERVER_HOST = "localhost";
final int SERVER_PORT = 12346;
// 初始化数据访问层和服务层对象
IClientDataAccess dataAccess = new ClientSocketDataAccess();
IClientService clientService = new ChatClientService(dataAccess);
try {
// 1. 连接到指定的服务器
clientService.connect(SERVER_HOST, SERVER_PORT);
System.out.println("已连接到聊天服务器!");
// 2. 启动后台线程以持续接收来自服务器的消息
clientService.startReceivingMessages();
// 3. 获取用户输入的用户名并发送给服务器
Scanner scanner = new Scanner(System.in);
String username = scanner.nextLine();
clientService.sendUserMessage(username); // 发送用户名
// 4. 主循环:持续读取用户输入并发送聊天消息
while (true) {
String message = scanner.nextLine();
clientService.sendUserMessage(message);
}
} catch (Exception e) {
// 捕获异常并输出错误信息,同时关闭数据访问连接
System.err.println("客户端异常: " + e.getMessage());
dataAccess.close();
}
}
}