1. 系统架构设计
1.1 C/S架构设计思路
- 描述从单机系统到C/S架构的改造思路
- 客户端和服务器端的职责划分
- 改造思路:将原系统拆分为客户端和服务器端两部分。原系统中的数据访问层 (DAO) 和业务逻辑层 (Service) 迁移至服务器端,客户端仅保留用户界面和网络通信模块,通过网络请求与服务器交互完成学生信息管理操作。
- 职责划分:
客户端:负责用户交互界面展示、接收用户输入、将用户操作转换为网络请求发送给服务器、接收服务器响应并展示结果。
服务器端:负责接收和解析客户端请求、处理核心业务逻辑 (学生信息的增删改查)、数据持久化操作、将处理结果返回给客户端。
1.2 系统架构图

架构说明:
- 客户端与服务器的通信方式
- 数据持久化层的设计
- 多线程处理机制
- 客户端与服务器通过 TCP Socket 进行通信,采用自定义协议格式交换数据。
- 数据持久化层保留原系统的多种实现 (文件、Excel、二进制等),由服务器统一管理。
- 服务器采用主线程监听连接,每接收一个客户端连接就创建一个新的处理线程,实现多客户端并发访问。
2. 关键问题
主要回答如下问题:
- 使用了字节流还是字符流来传递数据?简述I/O流应用于网络编程的好处?
- 如何使用多线程实现多客户端同时操作学生数据?多线程并发访问数据可能会带来什么问题?
- I/O 流选择:
使用了字符流(BufferedReader/PrintWriter)来传递数据。核心原因是我们自定义了文本格式的通信协议(基于字符串分割),字符流更适合处理文本类型的数据交换,且能直接与 readLine() / println() 配合,通过换行符识别消息边界,简化消息分割逻辑。 - 多线程实现与并发问题:服务器端使用主线程循环监听端口,每当有新客户端连接时,创建一个新的 ClientHandler 线程专门处理该客户端的所有请求。多线程并发访问可能带来的问题:
- 数据一致性问题:多个线程同时修改数据可能导致数据错乱。
- 资源竞争:多个线程同时操作文件或数据库可能导致文件损坏或数据异常。
- 死锁风险:不当的同步机制可能导致线程死锁。
- 解决方案:在 DAO 层的共享数据操作(如 addStudent、updateStudent、removeStudent)中添加 synchronized 关键字,保证同一时间只有一个线程能修改数据,避免并发冲突。例如在 TextFileStudentDAOImpl 的 updateStudent 方法上添加 synchronized,防止多线程同时写入导致文件损坏。
3. 运行截图
1、添加学生

2、删除学生

3、查看所有学生

4、按姓名搜索学生

5、按专业搜索学生
(匹配到数据)

(未匹配到数据)

6、按 GPA 搜索学生

7、修改学生信息

8、服务器的多线程并发能力验证:(我分别用了一个 Main.java 类和 NetAssist 网络调试助手作为客户端对服务器同时发送请求)

补充:服务器(以 NetAssist 为例),对于每个合法请求都要按操作类型返回对应响应:
- ADD/DELETE/UPDATE 操作:成功时返回 SUCCESS|0|(因为这些操作不需要返回数据,数据长度为 0);失败时返回 FAIL|0|错误原因(比如 "未找到学生")。
- 查询类操作(GET_ALL/SEARCH_NAME 等):成功且有数据时返回 SUCCESS|数据长度|数据内容(比如SUCCESS|30|12,kyungsoo,music,5.0,23,male);无数据时返回 NO_DATA|0|。
- 错误情况(导致客户端卡住)
如果服务器收到请求后没返回任何内容,或返回的内容不符合[状态]|数据长度|数据内容格式,客户端的in.readLine()会一直阻塞(等待响应)。

注意事项:客户端通过 BufferedReader.readLine() 读取响应时,依赖换行符 \n 识别消息结束。
- TCPServer.java 类:服务器使用 PrintWriter.println() 发送响应(自动添加 \n),无需手动处理;
- NetAssist:若用 NetAssist 模拟服务器,需按 shift + enter 输入换行符后再发送,否则客户端会阻塞。
4. 关键代码解析
层级结构如下:

1、新增一个 TCPServer.java 类,负责监听客户端连接、多线程处理请求:
java
package server;
import dao.StudentDAOImpl;
import dao.impl.ListStudentDAOImpl; // 可替换为其他DAO实现(如TextFile/Excel)
import service.StudentService;
import java.net.ServerSocket;
import java.net.Socket;
public class TCPServer {
private static final int PORT = 8080; // 服务器端口
private StudentService studentService;
// 初始化:复用原有Service和DAO(选择一种存储方式,如List/文件)
public TCPServer() {
StudentDAOImpl dao = new ListStudentDAOImpl(); // 可替换为TextFile/Excel/BinaryDAO
this.studentService = new StudentService(dao);
}
// 启动服务器
public void start() {
try (ServerSocket serverSocket = new ServerSocket(PORT)) {
System.out.println("服务器启动,监听端口:" + PORT);
// 循环监听客户端连接(多线程)
while (true) {
Socket clientSocket = serverSocket.accept(); // 阻塞等待客户端连接
System.out.println("新客户端连接:" + clientSocket.getInetAddress());
// 启动新线程处理该客户端请求
new Thread(new ClientHandler(clientSocket, studentService)).start();
}
} catch (Exception e) {
e.printStackTrace();
}
}
public static void main(String[] args) {
new TCPServer().start();
}
}
2、新增一个客户端处理线程 ClientHandler.java 类,主要是负责解析客户端请求、调用 Service 处理、返回响应结果:
java
package server;
import entity.Student;
import service.StudentService;
import java.io.BufferedReader;
import java.io.IOException;
import java.io.InputStreamReader;
import java.io.PrintWriter;
import java.net.Socket;
import java.util.List;
public class ClientHandler implements Runnable {
private Socket clientSocket;
private StudentService studentService;
private BufferedReader in; // 读取客户端请求
private PrintWriter out; // 发送服务器响应
public ClientHandler(Socket socket, StudentService service) {
this.clientSocket = socket;
this.studentService = service;
try {
in = new BufferedReader(new InputStreamReader(socket.getInputStream()));
out = new PrintWriter(socket.getOutputStream(), true); // autoFlush=true
} catch (IOException e) {
e.printStackTrace();
}
}
@Override
public void run() {
String request;
try {
// 循环读取客户端请求(直到客户端断开)
while ((request = in.readLine()) != null) {
System.out.println("收到请求:" + request);
// 解析请求并处理
String response = handleRequest(request);
// 发送响应给客户端
out.println(response);
}
} catch (IOException e) {
System.out.println("客户端断开连接:" + clientSocket.getInetAddress());
} finally {
// 关闭资源
try {
in.close();
out.close();
clientSocket.close();
} catch (IOException e) {
e.printStackTrace();
}
}
}
// 解析请求并调用Service处理
private String handleRequest(String request) {
String[] parts = request.split("\\|", -1); // 按|分割,保留空参数
if (parts.length == 0) {
return "FAIL|0|无效请求格式";
}
String operation = parts[0];
try {
switch (operation) {
case "ADD": // 新增学生:ADD|id|name|major|gpa|age|gender
if (parts.length != 7) {
return "FAIL|0|新增参数错误(需6个属性)";
}
Student student = new Student(
parts[1], parts[2], parts[3],
Double.parseDouble(parts[4]),
Integer.parseInt(parts[5]),
parts[6]
);
studentService.addStudent(student);
return "SUCCESS|0|"; // 无返回数据
case "DELETE": // 删除学生:DELETE|id
if (parts.length != 2) {
return "FAIL|0|删除参数错误(需学号)";
}
boolean deleted = studentService.removeStudent(parts[1]);
return deleted ? "SUCCESS|0|" : "FAIL|0|未找到该学生";
case "GET_ALL": // 查询所有:GET_ALL|
List<Student> all = studentService.getStudents();
return formatStudentList(all);
case "SEARCH_NAME": // 按姓名查:SEARCH_NAME|name
if (parts.length != 2) {
return "FAIL|0|查询参数错误(需姓名)";
}
List<Student> byName = studentService.searchByName(parts[1]);
return formatStudentList(byName);
case "SEARCH_MAJOR": // 按专业查:SEARCH_MAJOR|major
if (parts.length != 2) {
return "FAIL|0|查询参数错误(需专业)";
}
List<Student> byMajor = studentService.searchByMajor(parts[1]);
return formatStudentList(byMajor);
case "SEARCH_GPA": // 按GPA查:SEARCH_GPA|gpa
if (parts.length != 2) {
return "FAIL|0|查询参数错误(需GPA)";
}
List<Student> byGpa = studentService.searchByGpa(Double.parseDouble(parts[1]));
return formatStudentList(byGpa);
case "UPDATE": // 修改学生:UPDATE|id|newName|newMajor|newGpa|newAge|newGender
if (parts.length != 7) {
return "FAIL|0|修改参数错误(需6个属性)";
}
List<Student> students = studentService.getStudents();
Student target = students.stream().filter(s -> s.getId().equals(parts[1])).findFirst().orElse(null);
if (target == null) {
return "FAIL|0|未找到该学生";
}
target.setName(parts[2]);
target.setMajor(parts[3]);
target.setGpa(Double.parseDouble(parts[4]));
target.setAge(Integer.parseInt(parts[5]));
target.setGender(parts[6]);
// 需DAO支持更新(原有DAO无update方法,需扩展,见下文修改)
studentService.updateStudent(target);
return "SUCCESS|0|";
default:
return "FAIL|0|不支持的操作类型";
}
} catch (NumberFormatException e) {
return "FAIL|0|参数格式错误(GPA/年龄需为数字)";
} catch (Exception e) {
return "FAIL|0|服务器处理失败:" + e.getMessage();
}
}
// 格式化学生列表为协议响应格式
private String formatStudentList(List<Student> students) {
if (students.isEmpty()) {
return "NO_DATA|0|";
}
StringBuilder sb = new StringBuilder();
for (Student s : students) {
sb.append(s.getId()).append(",")
.append(s.getName()).append(",")
.append(s.getMajor()).append(",")
.append(s.getGpa()).append(",")
.append(s.getAge()).append(",")
.append(s.getGender()).append("&&");
}
String data = sb.substring(0, sb.length() - 2); // 去掉最后一个&&
return "SUCCESS|" + data.length() + "|" + data;
}
}
3、为了支持修改操作,我们需要扩展原有 DAO 和 Service:
1)修改 DAO 接口:StudentDAOImpl.java
java
package dao;
import entity.Student;
import java.util.List;
public interface StudentDAOImpl {
void addStudent(Student student);
boolean removeStudent(String id);
List<Student> getStudents();
List<Student> searchByName(String name);
List<Student> searchByMajor(String major);
List<Student> searchByGpa(double gpa);
// 新增:修改学生信息
boolean updateStudent(Student student);
}
2)要同步其实现类所有 DAOImpl 的 update 方法,以TextFileStudentDAOImpl.java为例,其他实现类都是一样的:
java
package dao.impl;
import dao.StudentDAOImpl;
import entity.Student;
import java.io.*;
import java.util.List;
public class TextFileStudentDAOImpl implements StudentDAOImpl {
// 原有方法不变
// 新增update方法:删除旧数据并添加新数据
@Override
public boolean updateStudent(Student student) {
List<Student> students = getStudents();
for (int i = 0; i < students.size(); i++) {
if (students.get(i).getId().equals(student.getId())) {
students.set(i, student); // 替换旧学生对象
saveStudents(students); // 覆盖文件保存
return true;
}
}
return false; // 未找到该学生
}
}
4、修改 Service:StudentService.java:
java
package service;
import dao.StudentDAOImpl;
import entity.Student;
import java.util.List;
public class StudentService {
private StudentDAOImpl studentDAO;
// 原有方法不变
// 新增:转发修改请求到DAO
public boolean updateStudent(Student student) {
return studentDAO.updateStudent(student);
}
}
5、新增一个 TCPClient.java 类,主要是负责与服务器建立连接、发送请求、接收响应:
java
package client;
import java.io.BufferedReader;
import java.io.IOException;
import java.io.InputStreamReader;
import java.io.PrintWriter;
import java.net.Socket;
public class TCPClient {
private static final String SERVER_IP = "127.0.0.1"; // 服务器IP(本地测试)
private static final int SERVER_PORT = 8080; // 服务器端口
private Socket socket;
private BufferedReader in;
private PrintWriter out;
// 连接服务器
public boolean connect() {
try {
socket = new Socket(SERVER_IP, SERVER_PORT);
in = new BufferedReader(new InputStreamReader(socket.getInputStream()));
out = new PrintWriter(socket.getOutputStream(), true);
System.out.println("连接服务器成功");
return true;
} catch (IOException e) {
System.out.println("连接服务器失败:" + e.getMessage());
return false;
}
}
// 发送请求并获取响应
public String sendRequest(String request) {
try {
out.println(request); // 发送请求
return in.readLine(); // 接收响应(阻塞直到收到)
} catch (IOException e) {
e.printStackTrace();
return "FAIL|0|网络异常";
}
}
// 关闭连接
public void close() {
try {
in.close();
out.close();
socket.close();
} catch (IOException e) {
e.printStackTrace();
}
}
}
6、新增一个客户端 Service 代理类:NetworkStudentService.java 用来替代本地 Service 类,将界面的方法调用转为网络请求,屏蔽网络细节:
java
package client;
import entity.Student;
import java.util.ArrayList;
import java.util.List;
public class NetworkStudentService {
private TCPClient tcpClient;
public NetworkStudentService() {
this.tcpClient = new TCPClient();
// 初始化时连接服务器
if (!tcpClient.connect()) {
throw new RuntimeException("无法连接服务器,程序退出");
}
}
// 新增学生
public boolean addStudent(Student student) {
String request = String.format(
"ADD|%s|%s|%s|%s|%d|%s",
student.getId(), student.getName(), student.getMajor(),
student.getGpa(), student.getAge(), student.getGender()
);
String response = tcpClient.sendRequest(request);
return response.startsWith("SUCCESS");
}
// 删除学生
public boolean removeStudent(String id) {
String request = "DELETE|" + id;
String response = tcpClient.sendRequest(request);
return response.startsWith("SUCCESS");
}
// 查询所有学生
public List<Student> getStudents() {
String request = "GET_ALL|";
String response = tcpClient.sendRequest(request);
return parseStudentResponse(response);
}
// 按姓名查询
public List<Student> searchByName(String name) {
String request = "SEARCH_NAME|" + name;
String response = tcpClient.sendRequest(request);
return parseStudentResponse(response);
}
// 按专业查询
public List<Student> searchByMajor(String major) {
String request = "SEARCH_MAJOR|" + major;
String response = tcpClient.sendRequest(request);
return parseStudentResponse(response);
}
// 按GPA查询
public List<Student> searchByGpa(double gpa) {
String request = "SEARCH_GPA|" + gpa;
String response = tcpClient.sendRequest(request);
return parseStudentResponse(response);
}
// 修改学生
public boolean updateStudent(Student student) {
String request = String.format(
"UPDATE|%s|%s|%s|%s|%d|%s",
student.getId(), student.getName(), student.getMajor(),
student.getGpa(), student.getAge(), student.getGender()
);
String response = tcpClient.sendRequest(request);
return response.startsWith("SUCCESS");
}
// 解析服务器响应为学生列表
private List<Student> parseStudentResponse(String response) {
List<Student> students = new ArrayList<>();
String[] parts = response.split("\\|", 3); // 分割为[状态, 长度, 数据]
if (!parts[0].equals("SUCCESS")) {
return students; // 失败或无数据,返回空列表
}
String data = parts[2];
if (data.isEmpty()) {
return students;
}
String[] studentStrs = data.split("&&"); // 分割多个学生
for (String s : studentStrs) {
String[] attrs = s.split(","); // 分割单个学生属性
if (attrs.length == 6) {
students.add(new Student(
attrs[0], attrs[1], attrs[2],
Double.parseDouble(attrs[3]),
Integer.parseInt(attrs[4]),
attrs[5]
));
}
}
return students;
}
// 关闭连接
public void close() {
tcpClient.close();
}
}
7、修改 Main.java,替换 StudentService 类为 NetworkStudentService 类,新增修改选项:
java
package code;
import client.NetworkStudentService;
import entity.Student;
import java.util.List;
import java.util.Scanner;
public class Main {
public static void main(String[] args) {
Scanner scanner = new Scanner(System.in);
NetworkStudentService service = null;
// 初始化网络服务(连接服务器)
try {
service = new NetworkStudentService();
System.out.println("✅ 已成功连接到学生信息管理服务器");
} catch (RuntimeException e) {
System.out.println("❌ " + e.getMessage());
scanner.close();
return;
}
boolean running = true;
while (running) {
System.out.println("\n=====================================");
System.out.println(" 学生信息管理系统(网络版)");
System.out.println("=====================================");
System.out.println("1. 添加学生");
System.out.println("2. 删除学生");
System.out.println("3. 查看所有学生");
System.out.println("4. 按姓名搜索学生");
System.out.println("5. 按专业搜索学生");
System.out.println("6. 按 GPA 搜索学生");
System.out.println("7. 修改学生信息"); // 新增修改功能
System.out.println("8. 退出程序");
System.out.println("=====================================");
System.out.print("请选择操作(1-8):");
// 输入校验(避免非数字输入崩溃)
if (!scanner.hasNextInt()) {
System.out.println("❌ 输入无效!请输入数字 1-8");
scanner.nextLine(); // 清空无效输入
continue;
}
int operation = scanner.nextInt();
scanner.nextLine(); // 吸收换行符
switch (operation) {
// 1. 添加学生
case 1:
Student student = new Student();
System.out.print("请输入学号:");
student.setId(scanner.nextLine().trim());
System.out.print("请输入姓名:");
student.setName(scanner.nextLine().trim());
System.out.print("请输入专业:");
student.setMajor(scanner.nextLine().trim());
// GPA 输入校验
double gpa = 0.0;
while (true) {
System.out.print("请输入 GPA(0.0-5.0):");
if (scanner.hasNextDouble()) {
gpa = scanner.nextDouble();
if (gpa >= 0.0 && gpa <= 5.0) {
break;
} else {
System.out.println("❌ GPA 范围无效!请输入 0.0-5.0 之间的数值");
}
} else {
System.out.println("❌ 输入无效!请输入数字");
scanner.nextLine(); // 清空无效输入
}
}
student.setGpa(gpa);
scanner.nextLine(); // 吸收换行符
// 年龄输入校验
int age = 0;
while (true) {
System.out.print("请输入年龄(15-40):");
if (scanner.hasNextInt()) {
age = scanner.nextInt();
if (age >= 15 && age <= 40) {
break;
} else {
System.out.println("❌ 年龄范围无效!请输入 15-40 之间的数值");
}
} else {
System.out.println("❌ 输入无效!请输入数字");
scanner.nextLine(); // 清空无效输入
}
}
student.setAge(age);
scanner.nextLine(); // 吸收换行符
System.out.print("请输入性别:");
student.setGender(scanner.nextLine().trim());
// 调用网络服务添加学生
if (service.addStudent(student)) {
System.out.println("✅ 学生添加成功!");
} else {
System.out.println("❌ 学生添加失败!");
}
break;
// 2. 删除学生
case 2:
System.out.print("请输入要删除的学生学号:");
String deleteId = scanner.nextLine().trim();
if (service.removeStudent(deleteId)) {
System.out.println("✅ 学生删除成功!");
} else {
System.out.println("❌ 未找到该学号的学生,删除失败!");
}
break;
// 3. 查看所有学生
case 3:
List<Student> allStudents = service.getStudents();
if (allStudents.isEmpty()) {
System.out.println("📭 暂无学生信息");
} else {
System.out.println("\n===== 所有学生信息 =====");
for (Student s : allStudents) {
System.out.println(s); // 复用 Student 的 toString 方法
}
}
break;
// 4. 按姓名搜索学生
case 4:
System.out.print("请输入要搜索的学生姓名(支持模糊匹配):");
String searchName = scanner.nextLine().trim();
List<Student> nameStudents = service.searchByName(searchName);
if (nameStudents.isEmpty()) {
System.out.println("📭 未找到匹配姓名的学生");
} else {
System.out.println("\n===== 搜索结果(姓名包含:" + searchName + ")=====");
for (Student s : nameStudents) {
System.out.println(s);
}
}
break;
// 5. 按专业搜索学生
case 5:
System.out.print("请输入要搜索的专业(支持模糊匹配):");
String searchMajor = scanner.nextLine().trim();
List<Student> majorStudents = service.searchByMajor(searchMajor);
if (majorStudents.isEmpty()) {
System.out.println("📭 未找到匹配专业的学生");
} else {
System.out.println("\n===== 搜索结果(专业包含:" + searchMajor + ")=====");
for (Student s : majorStudents) {
System.out.println(s);
}
}
break;
// 6. 按 GPA 搜索学生
case 6:
double searchGpa = 0.0;
while (true) {
System.out.print("请输入要搜索的 GPA(0.0-5.0):");
if (scanner.hasNextDouble()) {
searchGpa = scanner.nextDouble();
if (searchGpa >= 0.0 && searchGpa <= 5.0) {
break;
} else {
System.out.println("❌ GPA 范围无效!请输入 0.0-5.0 之间的数值");
}
} else {
System.out.println("❌ 输入无效!请输入数字");
scanner.nextLine(); // 清空无效输入
}
}
scanner.nextLine(); // 吸收换行符
List<Student> gpaStudents = service.searchByGpa(searchGpa);
if (gpaStudents.isEmpty()) {
System.out.println("📭 未找到该 GPA 的学生");
} else {
System.out.println("\n===== 搜索结果(GPA:" + searchGpa + ")=====");
for (Student s : gpaStudents) {
System.out.println(s);
}
}
break;
// 7. 修改学生信息(新增功能)
case 7:
System.out.print("请输入要修改的学生学号:");
String updateId = scanner.nextLine().trim();
// 步骤1:查询该学号是否存在(通过获取所有学生过滤)
List<Student> all = service.getStudents();
Student targetStudent = null;
for (Student s : all) {
if (s.getId().equals(updateId)) {
targetStudent = s;
break;
}
}
if (targetStudent == null) {
System.out.println("❌ 未找到学号为 " + updateId + " 的学生,修改失败!");
break;
}
// 步骤2:显示原信息,引导输入新信息
System.out.println("\n===== 当前学生信息 =====");
System.out.println(targetStudent);
System.out.println("===== 请输入新信息(直接回车保留原信息)=====");
// 输入新姓名(支持保留原信息)
System.out.print("新姓名(原:" + targetStudent.getName() + "):");
String newName = scanner.nextLine().trim();
if (newName.isEmpty()) {
newName = targetStudent.getName();
}
// 输入新专业
System.out.print("新专业(原:" + targetStudent.getMajor() + "):");
String newMajor = scanner.nextLine().trim();
if (newMajor.isEmpty()) {
newMajor = targetStudent.getMajor();
}
// 输入新 GPA(校验逻辑)
double newGpa = targetStudent.getGpa();
while (true) {
System.out.print("新 GPA(原:" + targetStudent.getGpa() + ",直接回车保留):");
String gpaInput = scanner.nextLine().trim();
if (gpaInput.isEmpty()) {
break; // 保留原 GPA
}
try {
newGpa = Double.parseDouble(gpaInput);
if (newGpa >= 0.0 && newGpa <= 4.0) {
break;
} else {
System.out.println("❌ GPA 范围无效!请输入 0.0-4.0 之间的数值");
}
} catch (NumberFormatException e) {
System.out.println("❌ 输入无效!请输入数字或直接回车");
}
}
// 输入新年龄(校验逻辑)
int newAge = targetStudent.getAge();
while (true) {
System.out.print("新年龄(原:" + targetStudent.getAge() + ",直接回车保留):");
String ageInput = scanner.nextLine().trim();
if (ageInput.isEmpty()) {
break; // 保留原年龄
}
try {
newAge = Integer.parseInt(ageInput);
if (newAge >= 15 && newAge <= 40) {
break;
} else {
System.out.println("❌ 年龄范围无效!请输入 15-40 之间的数值");
}
} catch (NumberFormatException e) {
System.out.println("❌ 输入无效!请输入数字或直接回车");
}
}
// 输入新性别
System.out.print("新性别(原:" + targetStudent.getGender() + "):");
String newGender = scanner.nextLine().trim();
if (newGender.isEmpty()) {
newGender = targetStudent.getGender();
}
// 步骤3:构建修改后的学生对象,调用网络服务修改
Student updatedStudent = new Student(
updateId, newName, newMajor, newGpa, newAge, newGender
);
if (service.updateStudent(updatedStudent)) {
System.out.println("✅ 学生信息修改成功!");
System.out.println("修改后信息:" + updatedStudent);
} else {
System.out.println("❌ 学生信息修改失败!");
}
break;
// 8. 退出程序
case 8:
running = false;
service.close(); // 关闭网络连接
System.out.println("👋 程序已退出,感谢使用!");
break;
// 无效操作
default:
System.out.println("❌ 无效操作!请输入数字 1-8");
}
// 每次操作后暂停,提升用户体验
if (running) {
System.out.println("\n按 Enter 键继续...");
scanner.nextLine();
}
}
scanner.close();
}
}
4.1 客户端与服务器端通信(以某一个具体功能说明)
以"添加学生"为例:
-
客户端流程:
- 用户在控制台输入学生信息(学号12、姓名 kyungsoo 等);
- NetworkStudentService 将信息拼接为协议格式的请求字符串:ADD|12|kyungsoo|music|5.0|23|male;
- TCPClient 通过 PrintWriter.println() 发送请求(自动添加 \n );
- 客户端阻塞等待服务器响应,接收后解析状态(SUCCESS/FAIL)并显示结果。
-
服务器端流程:
- TCPServer 监听8888端口,接收客户端连接后创建 ClientHandler 线程;
- ClientHandler 通过 BufferedReader.readLine() 读取请求字符串;
- 按 | 分割字符串,解析操作类型(ADD)和参数,调用 StudentService.addStudent() 处理业务;
- 处理完成后,返回协议格式的响应: SUCCESS|0| ;
- 通过 PrintWriter.println() 发送响应(自动添加 \n ),客户端接收后继续执行。
4.2 服务器端将从 Socket 读取到的数据存入本地
服务器端数据持久化流程:
服务器端接收客户端请求后,通过 "Service→DAO" 的调用链将数据存入本地存储(以 TextFileStudentDAOImpl 为例):
- ClientHandler 解析请求参数,构建 Student 对象;
- 调用 StudentService.addStudent() ,将对象传递给 TextFileStudentDAOImpl ;
- TextFileStudentDAOImpl 读取本地文件(如 students.txt )中的现有数据,添加新学生对象;
- 通过 BufferedWriter 将更新后的学生列表覆盖写入文件,完成数据持久化;
- 若使用 ListStudentDAOImpl ,则数据暂存于内存List中(程序重启后丢失),其他DAO实现(Excel/Binary)流程类似,仅存储格式不同。
4.3 服务器端如何支持多个客户端同时访问
多客户端并发访问实现
-
核心设计:主线程监听+子线程处理
- TCPServer 的 start() 方法中, ServerSocket.accept() 阻塞等待客户端连接;
- 每接收一个连接,创建一个独立的 ClientHandler 线程(实现 Runnable 接口),专门处理该客户端的所有请求;
- 主线程继续监听新连接,子线程并行处理已有客户端的请求,实现多客户端并发。
-
并发安全保障:
- 所有客户端共享 StudentService 和 DAO 实例,数据操作(增删改查)存在并发风险;
- 在DAO层的核心方法( addStudent 、 updateStudent 、 removeStudent 、 saveStudents )上添加 synchronized 关键字,确保同一时间只有一个线程能执行修改操作,避免数据错乱或文件损坏。
线程冲突的示例(并发修改同一学生):

从运行截图上看,我们这里并未出现线程冲突,尽管两个客户端同时发送更新学生信息的请求,但依然无异常报错,正常返回两个 SUCCESS|0|。这就是 synchronized 关键字的功劳了。
在 DAO 层的 updateStudent 方法上添加 synchronized 后,"读 - 改 - 写" 操作会被锁定为原子操作:
- 当线程 A 执行 updateStudent 时,会锁住 DAO 对象,线程 B 必须等待线程 A 执行完整个方法,才能执行updateStudent;
- 此时步骤变为:
线程 A 读取→修改→写入,全程独占 DAO 对象;
线程 B 等待线程 A 完成后,再读取(以GPA为例:此时读取的是线程 A 修改后的 4.8)→修改→写入;
最终 GPA 是 4.5(符合 "后执行的请求覆盖先执行的" 逻辑),不会出现数据错乱。
5. 遇到的问题及解决方法
1、因为手误不小心关闭了 NetAssist 但并未解除对8080端口的占用导致后续实验连接不了8080端口。解决方法:
- 按下 Win+R 打开运行窗口,输入 cmd 打开命令提示符。
- 输入命令netstat -ano | findstr :8080,执行后会显示占用 8080 端口的进程 PID,比如输出结果最后一列的数字 4567 就是对应的 PID。
- 可输入tasklist | findstr PID(将 4567 替换为实际 PID)确认进程名称。
输入命令taskkill /PID 4567 /F(替换 PID 为实际数值),即可强制终止该进程,释放 8080 端口。也能按下 Ctrl+Shift+Esc 打开任务管理器,在 "详细信息" 栏找到对应 PID 的进程,右键选择 "结束任务"。

6. 总结
6.1 学到的知识
- 理解C/S架构的核心思想:客户端负责交互,服务器端负责业务逻辑和数据存储,通过网络协议实现通信;
- 掌握TCP Socket网络编程:基于字符流的客户端/服务器通信实现,消息边界识别(换行符),多线程并发处理;
- 自定义通信协议设计:通过字符串分割格式定义请求/响应,实现标准化数据交换;
- 并发安全处理:使用 synchronized 关键字保证共享数据操作的原子性,避免并发冲突;
- 问题排查能力:解决端口占用、网络阻塞、数据持久化失败等实际问题。
6.2 改进方向
- 协议优化:当前使用自定义文本协议,可替换为成熟的应用层协议(如HTTP、JSON-RPC),简化数据序列化/反序列化逻辑,提高兼容性;
- 数据持久化优化:替换文件存储为数据库(如MySQL),支持更复杂的查询和事务,避免文件损坏风险;
- 并发性能优化:使用线程池替代手动创建线程,减少线程创建销毁的开销,支持更多客户端并发;
- 异常处理增强:添加客户端重连机制、服务器请求超时处理、数据校验(如学号唯一),提高系统稳定性;
- 界面优化:将控制台客户端改为GUI客户端(复用原有 MainFrame 、 AddStudentDialog 等类),提升用户体验。
6.3 关于应用层协议改写的思考(结合 AI 建议)
当前系统使用自定义文本协议,虽简单易实现,但存在扩展性差、兼容性弱的问题。若用应用层协议改写,推荐以下方案:
- 使用HTTP协议:客户端通过HTTP请求(GET/POST)发送操作指令,服务器端使用Spring Boot搭建HTTP接口,返回JSON格式数据;
- 优势:通用性强,支持跨平台(如前端页面作为客户端),有成熟的框架支持(Spring Boot、OkHttp);
- 实现要点:服务器端定义接口(如 /student/add 、 /student/getAll ),客户端用 OkHttp 发送请求,解析JSON响应。
- 使用JSON格式优化自定义协议:保留TCP通信,将请求 / 响应数据改为JSON格式(如 {"operation":"ADD","data":{"id":"12","name":"kyungsoo"}} ),使用Jackson库序列化 / 反序列化;
- 优势:支持复杂数据结构,可读性强,比字符串分割更易维护;
- 实现要点:实体类添加Jackson注解,服务器端接收JSON字符串后反序列化为对象,处理后序列化返回。