学生信息管理系统——C/S架构
学生信息管理系统——C/S架构
1. 系统架构设计
1.1 C/S架构设计思路
原有的学生信息管理系统通常运行在单机环境中,所有数据操作(增删改查)直接作用于Main.java文件。为了支持多用户远程访问与集中管理,本次改造采用客户端-服务器(Client/Server, C/S)架构:
服务器端:负责统一维护学生数据(如存储在内存 List ),接收来自多个客户端的请求,执行对应操作,并返回结果。它是系统的“核心引擎”和唯一数据源。
客户端:提供用户交互界面(本项目采用控制台界面以简化开发),负责收集用户输入、构造请求、发送至服务器,并展示服务器返回的结果。它不直接处理数据,仅作为“前端代理”。 这种分离使得系统具备可扩展性(支持多用户)、数据一致性(单一数据源)和安全性(敏感操作集中在服务端)。
1.2 系统架构图
+------------------+ TCP连接 +------------------+
| 客户端 (Client) | <------------------> | 服务器 (Server) |
| - 控制台UI | | - 主监听线程 |
| - 请求构造 | | - 多工作线程池 |
| - 结果展示 | | - 学生数据管理器 |
+------------------+ +------------------+
架构说明:
通信方式:基于 Java Socket 和 ServerSocket 实现 TCP 长连接,确保可靠传输。
多线程处理机制:服务器主循环接受新连接后,为每个客户端创建一个独立的 HandlerThread 线程处理其全部请求,实现并发支持。
注释:本次未实现数据持久化,可参考文本文件与基于二进制文件的存储的学生管理系统来进行自我改造,实现数据持久化。
1.3 包结构

2.问题解答
2.1 使用了字节流还是字符流来传递数据?简述I/O流应用于网络编程的好处?
本系统采用 字符流(BufferedReader / PrintWriter) 进行数据传输。虽然底层 TCP 是字节流,但上层使用字符流更便于处理文本协议(如 "1;张三;20;男;12345;数学;4.2")。
好处包括:
可读性强:调试时可直接查看传输内容;
开发便捷:无需手动序列化/反序列化对象,适合轻量级协议。

2.2 如何使用多线程实现多客户端同时操作学生数据?多线程并发访问数据可能会带来什么问题?
实现方式: 服务器每接受一个 Socket 连接,就启动一个新线程(或从线程池分配)专门处理该客户端的所有请求。每个线程独立运行,互不阻塞。
并发问题: 多个线程同时读写共享的学生列表(如 ArrayList)可能导致 数据不一致(如丢失更新、脏读)甚至 ConcurrentModificationException。
解决方案: 对关键操作(增删改查)加同步锁(synchronized 块)或使用线程安全容器(如 Collections.synchronizedList()),确保同一时间只有一个线程修改数据。
服务器启动: 显示“服务器已启动,监听端口 8080”
客户端连接: 输入名字查询,返回“Student{name='张三', age=20, gender='男', id='12345', major='数学', gpa=4.2}

多客户端测试: 两个终端同时添加学生,服务器日志显示两条独立处理记录

异常处理: 客户端输入非法指令,服务器返回“无效操作”
运行截图

3. 关键代码解析
3.1 客户端与服务器端通信
以“查询学生姓名”功能为例:
客户端:用户输入”张三“,通过 PrintWriter 发送至服务器。
服务器:HandlerThread 读取该行,解析指令类型和参数,在学生列表中查找姓名为 ”张三“的记录,若存在则返回 "Student{name='张三', age=20, gender='男', id='12345', major='数学', gpa=4.2}",否则返回 "No students found!"。
客户端:解析响应,打印结果或提示未找到。
// 客户端发送请求
out.println(command);
out.flush();
// 服务器处理请求(HandlerThread.run() 中)
String[] parts = command.split(";");
case "3":
// Search for a student by name
String searchName = parts[1];
List<Student> searchResults = studentDAO.searchByName(searchName);
if (searchResults.isEmpty()) {
response.append("No students found!\n");
} else {
System.out.println("Search results:");
for (Student s : searchResults) {
response.append(s).append("\n");
}
}
break;
3.2 服务器端将从Socket读取到的数据存入本地服务器
当收到 添加学生 请求时:
解析参数创建 Student 对象;
调用 StudentManagementSystem.add(student);
add() 方法内部将学生加入列表。
public void addStudent(Student student) {
synchronized (student) {
studentDAO.addStudent(student);
}
}
使用synchronized进行线程保护。
3.3 服务器端支持多个客户端同时访问
服务器主程序使用无限循环监听端口:
private StudentManagementSystem studentDAO=new StudentManagementSystem();
private static ExecutorService threadPool = Executors.newCachedThreadPool();
public StudentSever() {
}
public static void main(String[] args) {
System.out.println("Student Server started");
StudentSever server = new StudentSever();
try (ServerSocket serverSocket = new ServerSocket(PORT)) {
while (true) {
Socket clientSocket = serverSocket.accept();
threadPool.submit(new ClientHandler(clientSocket,server));
}
} catch (IOException e) {
System.err.println("Server exception: " + e.getMessage());
} finally {
threadPool.shutdown();
}
}
每个 ClientHandler 线程独立持有自己的 Socket、BufferedReader 和 PrintWriter,处理该客户端的完整会话(支持多次操作直至退出)。
4.结果展示
4.1 添加学生

4.2 姓名查找

4.3 学科查找

4.4 绩点查找

4.5 所有数据

4.6 数据协议

5.遇到的问题及解决方法
- 问题:客户端进行选择时,终端会出现空行跳出,没有出现数据,在进行相同功能,数据出现
原因:response.append("xxx\n")时添加换行符,而out.println(response)又会额外再加 1 个换行符 → 最终服务器发送的响应是xxx\n\n(两行),导致**客户端读取错位 **。
解决:严格约定每条请求为单行文本,客户端每次发送后调用 flush(),服务器按行解析。避免在一条消息中包含换行符。
代码错误点:
StudentClient.java
out.println(command);
out.flush();
return in.readLine();
StudentSever.java
public void run() {
....
while ((inputLine = in.readLine()) != null) {
System.out.println("Received: " + inputLine);
String response= server.processCommand(inputLine);
out.println(response);
}
....
}
private String processCommand(String command) {
....
case "1":
// Add a student
Student student = parseStudent(parts);
studentDAO.addStudent(student);
response.append("Student added successfully!\n");
break;
....
}
代码修改后:
StudentSever.java
public void run() {
....
while ((inputLine = in.readLine()) != null) {
System.out.println("Received: " + inputLine);
String response= server.processCommand(inputLine);
out.println(response);
}
....
}
private String processCommand(String command) {
....
case "1":
// Add a student
Student student = parseStudent(parts);
studentDAO.addStudent(student);
response.append("Student added successfully!");
break;
....
}
错误展示:

问题:客户端选择展示全部数据时,终端会只出现一条,再选择展示全部数据,再出现一条,直到存入的数据都展示一遍,最后输出The System Data is empty Now!
原因:使用return in.readLine(),而为了展示美观,在student的toString最后加上了换行符,而 in.readLine()读取时遇到换行符就结束。
解决:添加特殊标识符。
代码错误点:
StudentClient.java
out.println(command);
out.flush();
return in.readLine();
StudentSever.java
case "6":
// Show all Students
List<Student> studentList = studentDAO.getStudents();
if (studentList.size() == 0) {
response.append("The System Data is empty Now!");
}else {
for (Student studentItem : studentList) {
response.append(studentItem.toString());
}
}
break;
代码修改后:
StudentClient.java
out.println(command);
out.flush();
String line;
StringBuilder response = new StringBuilder();
while(!(line = in.readLine()).equals("###END###")) {
response.append(line).append("\n");
}
return response.toString();
StudentSever.java
case "6":
// Show all Students
List<Student> studentList = studentDAO.getStudents();
if (studentList.size() == 0) {
response.append("The System Data is empty Now!\n");
}else {
for (Student studentItem : studentList) {
response.append(studentItem.toString()).append("\n");
}
}
break;
....
response.append("###END###");
错误展示:

6.完整代码
StudentDAO.java
package dao;
import java.util.List;
import model.Student;
public interface StudentDAO {
void addStudent(Student student);
void removeStudent(String id);
List<Student> getStudents();
List<Student> searchByName(String name);
List<Student> searchByMajor(String major);
List<Student> searchByGpa(double gpa);
}
StudentDAOImpl.java
package dao;
import model.Student;
import java.util.ArrayList;
import java.util.List;
public class StudentDAOImpl implements StudentDAO {
private List<Student> students;
public StudentDAOImpl() {
students = new ArrayList<>();
}
@Override
public void addStudent(Student student) {
students.add(student);
}
@Override
public void removeStudent(String id) {
students.removeIf(student -> student.getId().equals(id));
}
@Override
public List<Student> getStudents() {
return new ArrayList<>(students);
}
@Override
public List<Student> searchByName(String name) {
List<Student> result = new ArrayList<>();
for (Student student : students) {
if (student.getName().equals(name)) {
result.add(student);
}
}
return result;
}
@Override
public List<Student> searchByMajor(String major) {
List<Student> result = new ArrayList<>();
for (Student student : students) {
if (student.getMajor().equals(major)) {
result.add(student);
}
}
return result;
}
@Override
public List<Student> searchByGpa(double gpa) {
List<Student> result = new ArrayList<>();
for (Student student : students) {
if (student.getGpa() == gpa) {
result.add(student);
}
}
return result;
}
}
Student.java
package model;
public class Student {
private String name;
private int age;
private String gender;
private String id;
private String major;
private double gpa;
public Student(String name, int i, String gender, String id, String major, double d) {
this.name = name;
this.age = i;
this.gender = gender;
this.id = id;
this.major = major;
this.gpa = d;
}
public String getName() {
return name;
}
public void setName(String name) {
this.name = name;
}
public int getAge() {
return age;
}
public void setAge(int age) {
this.age = age;
}
public String getGender() {
return gender;
}
public void setGender(String gender) {
this.gender = gender;
}
public String getId() {
return id;
}
public void setId(String id) {
this.id = id;
}
public String getMajor() {
return major;
}
public void setMajor(String major) {
this.major = major;
}
public double getGpa() {
return gpa;
}
public void setGpa(double gpa) {
this.gpa = gpa;
}
@Override
public String toString() {
return String.format(
"Student{" +
"name='" + name + '\'' +
", age=" + age +
", gender='" + gender + '\'' +
", id='" + id + '\'' +
", major='" + major + '\'' +
", gpa=" + gpa +
'}'
);
}
}
StudentClient.java
package network;
import java.io.*;
import java.net.*;
import java.util.*;
public class StudentClient {
private static final String SERVER_ADDRESS = "127.0.0.1";
private static final int SERVER_PORT =8080;
private Socket socket;
private Scanner scanner;
private BufferedReader in;
private PrintWriter out;
public StudentClient() {
scanner = new Scanner(System.in);
}
public void start() {
try {
socket = new Socket(SERVER_ADDRESS, SERVER_PORT);
in = new BufferedReader(
new InputStreamReader(socket.getInputStream()));
out = new PrintWriter(socket.getOutputStream(), true);
System.out.println("Connected to Student Management System Server");
boolean running = true;
while (running) {
showMenu();
int choice = scanner.nextInt();
scanner.nextLine();
switch (choice) {
case 1:
addStudent();
break;
case 2:
removeStudent();
break;
case 3:
searchByName();
break;
case 4:
searchByMajor();
break;
case 5:
searchByGpa();
break;
case 6:
showAllStudents();
break;
case 7:
sendCommand("EXIT");
running = false;
break;
default:
System.out.println("Invalid choice!");
}
}
} catch (IOException e) {
System.err.println("Client exception: " + e.getMessage());
} finally {
closeConnections();
}
}
private void showMenu() {
System.out.println("Enter 1 to add a student");
System.out.println("Enter 2 to remove a student");
System.out.println("Enter 3 to search for a student by name");
System.out.println("Enter 4 to search for a student by major");
System.out.println("Enter 5 to search for a student by GPA");
System.out.println("Enter 6 to show all students");
System.out.println("Enter 7 to exit");
System.out.print("Enter your choice: ");
}
private void addStudent() {
System.out.print("Enter student name: ");
String name = scanner.nextLine();
System.out.print("Enter student age: ");
int age = scanner.nextInt();
scanner.nextLine(); // consume newline
System.out.print("Enter student gender: ");
String gender = scanner.nextLine();
System.out.print("Enter student ID: ");
String id = scanner.nextLine();
System.out.print("Enter student major: ");
String major = scanner.nextLine();
System.out.print("Enter student GPA: ");
double gpa = scanner.nextDouble();
scanner.nextLine(); // consume newline
String command = String.format("1;%s;%d;%s;%s;%s;%.2f",
name, age, gender, id, major, gpa);
String response = sendCommand(command);
System.out.println(response);
}
private void removeStudent() {
System.out.print("Enter student ID to remove: ");
String id = scanner.nextLine();
String response = sendCommand("2;" + id);
System.out.println(response);
}
private void searchByName() {
System.out.print("Enter student name to search: ");
String name = scanner.nextLine();
String response = sendCommand("3;" + name);
System.out.println(response);
}
private void searchByMajor() {
System.out.print("Enter student major to search: ");
String major = scanner.nextLine();
String response = sendCommand("4;" + major);
System.out.println(response);
}
private void searchByGpa() {
System.out.print("Enter student GPA to search: ");
double gpa = scanner.nextDouble();
scanner.nextLine();
String response = sendCommand("5;" + gpa);
System.out.println(response);
}
private void showAllStudents() {
String response = sendCommand("6");
if (response.isEmpty() || response.equals("The System Data is empty Now!")) {
System.out.println("No students found!");
} else {
System.out.println("All students:");
System.out.println(response);
}
}
private String sendCommand(String command) {
try {
out.println(command);
out.flush();
String line;
StringBuilder response = new StringBuilder();
while(!(line = in.readLine()).equals("###END###")) {
response.append(line).append("\n");
}
return response.toString();
} catch (IOException e) {
closeConnections();
return "Error: " + e.getMessage();
}
}
private void closeConnections() {
try {
if (in != null) in.close();
if (out != null) out.close();
if (socket != null) socket.close();
if (scanner != null) scanner.close();
} catch (IOException e) {
System.err.println("Error closing connections: " + e.getMessage());
}
}
public static void main(String[] args) {
StudentClient client = new StudentClient();
client.start();
}
}
StudentSever.java
package network;
import java.util.*;
import java.util.concurrent.ExecutorService;
import java.util.concurrent.Executors;
import model.Student;
import service.StudentManagementSystem;
import dao.StudentDAO;
import dao.StudentDAOImpl;
import java.io.*;
import java.net.ServerSocket;
import java.net.Socket;
public class StudentSever {
private static final int PORT = 8080;
private StudentManagementSystem studentDAO=new StudentManagementSystem();
private static ExecutorService threadPool = Executors.newCachedThreadPool();
public StudentSever() {
}
public static void main(String[] args) {
System.out.println("Student Server started");
StudentSever server = new StudentSever();
try (ServerSocket serverSocket = new ServerSocket(PORT)) {
while (true) {
Socket clientSocket = serverSocket.accept();
threadPool.submit(new ClientHandler(clientSocket,server));
}
} catch (IOException e) {
System.err.println("Server exception: " + e.getMessage());
} finally {
threadPool.shutdown();
}
}
private static class ClientHandler implements Runnable {
private Socket clientSocket;
private StudentSever server;
public ClientHandler(Socket socket,StudentSever server) {
this.clientSocket = socket;
this.server = server;
}
@Override
public void run() {
try (BufferedReader in = new BufferedReader(new InputStreamReader(clientSocket.getInputStream()));
PrintWriter out = new PrintWriter(clientSocket.getOutputStream(), true)) {
String inputLine;
while ((inputLine = in.readLine()) != null) {
System.out.println("Received: " + inputLine);
String response= server.processCommand(inputLine);
out.println(response);
}
} catch (IOException e) {
System.err.println("Client handler exception: " + e.getMessage());
} finally {
try {
clientSocket.close();
} catch (IOException e) {
System.err.println("Could not close client socket: " + e.getMessage());
}
}
}
}
private String processCommand(String command) {
String[] parts = command.split(";");
String choice = parts[0];
StringBuilder response = new StringBuilder();
try {
switch (choice) {
case "1":
// Add a student
Student student = parseStudent(parts);
studentDAO.addStudent(student);
response.append("Student added successfully!\n");
break;
case "2":
// Remove a student
String removeId = parts[1];
List<Student> students = studentDAO.getStudents();
boolean removed = false;
for (Student s : students) {
if (s.getId().equals(removeId)) {
studentDAO.removeStudent(s);
removed = true;
response.append("Student removed successfully!\n");
break;
}
}
if (!removed) {
response.append("Student not found!\n");
}
break;
case "3":
// Search for a student by name
String searchName = parts[1];
List<Student> searchResults = studentDAO.searchByName(searchName);
if (searchResults.isEmpty()) {
response.append("No students found!\n");
} else {
System.out.println("Search results:");
for (Student s : searchResults) {
response.append(s).append("\n");
}
}
break;
case "4":
// Search for a student by major
String searchMajor = parts[1];
searchResults = studentDAO.searchByMajor(searchMajor);
if (searchResults.isEmpty()) {
response.append("No students found!\n");
} else {
System.out.println("Search results:");
for (Student s : searchResults) {
response.append(s).append("\n");
}
}
break;
case "5":
// Search for a student by GPA
double searchGpa = Double.parseDouble(parts[1]);
searchResults = studentDAO.searchByGpa(searchGpa);
if (searchResults.isEmpty()) {
response.append("No students found!\n");
} else {
System.out.println("Search results:");
for (Student s : searchResults) {
response.append(s).append("\n");
}
}
break;
case "6":
// Show all Students
List<Student> studentList = studentDAO.getStudents();
if (studentList.size() == 0) {
response.append("The System Data is empty Now!\n");
}else {
for (Student studentItem : studentList) {
response.append(studentItem.toString()).append("\n");
}
}
break;
case "7":
// Exit the program
response.append("Exit Successfully!\n");
break;
default:
// Invalid input
response.append("Invalid choice!\n");
break;
}
} catch (Exception e) {
response.append("Error processing command: " + e.getMessage()).append("\n");
}
response.append("###END###");
return response.toString();
}
private Student parseStudent(String[] parts) {
Student student = new Student(
parts[1], // name
Integer.parseInt(parts[2]), // age
parts[3],
parts[4],
parts[5],
Double.parseDouble(parts[6]) // gpa
);
return student;
}
}
StudentManagementSystem.java
package service;
import dao.StudentDAO;
import dao.StudentDAOImpl;
import model.Student;
import java.util.ArrayList;
import java.util.Collections;
import java.util.List;
public class StudentManagementSystem {
private StudentDAO studentDAO;
private List<Student> students = Collections.synchronizedList(new ArrayList<>());
public StudentManagementSystem() {
this.studentDAO = new StudentDAOImpl();
}
public void addStudent(Student student) {
synchronized (student) {
studentDAO.addStudent(student);
}
}
public void removeStudent(Student student) {
synchronized (student) {
studentDAO.removeStudent(student.getId());
}
}
public List<Student> getStudents() {
return studentDAO.getStudents();
}
public List<Student> searchByName(String name) {
return studentDAO.searchByName(name);
}
public List<Student> searchByMajor(String major) {
return studentDAO.searchByMajor(major);
}
public List<Student> searchByGpa(double gpa) {
return studentDAO.searchByGpa(gpa);
}
}
7.总结
通过本次作业,我深入理解了 C/S 架构的核心思想、TCP 网络编程的实现细节以及多线程并发控制的重要性。不仅掌握了 Socket、Thread、synchronized 等关键技术,还提升了系统设计和调试能力。
可改进之处:
使用 GUI(如 Swing)提升客户端体验;
引入连接池优化线程资源;
采用 JSON 或自定义二进制协议提高传输效率;
增加用户认证与操作日志。
数据持久化。
附加问答:能否使用应用层的网络协议来改写该系统?
完全可以。当前系统基于原始 TCP 自定义文本协议,属于应用层协议的自行实现。若改用标准应用层协议,可考虑:
HTTP/HTTPS:将服务器改造为 RESTful API(如使用 Spring Boot),客户端通过 HTTP 请求(GET/POST/PUT/DELETE)操作学生资源。优势是兼容性强、工具链丰富、天然支持 Web 客户端。
WebSocket:适用于需要双向实时通信的场景(如通知推送)。
自定义二进制协议:如基于 Protobuf 或自定义帧结构,提升性能与安全性。
改用标准协议能显著降低开发复杂度、提升互操作性,尤其适合构建跨平台或互联网级应用。但对于教学目的或小型局域网系统,原始 TCP + 自定义协议仍是一种简洁有效的方案。

浙公网安备 33010602011771号