将学生管理系统改造为C/S模式 - 开发过程报告

1. 系统架构设计

1.1 C/S架构设计思路

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

1.2 系统架构图

架构说明:

  • 客户端与服务器的通信方式
  • 数据持久化层的设计
  • 多线程处理机制
  1. 客户端与服务器通过 TCP Socket 进行通信,采用自定义协议格式交换数据。
  2. 数据持久化层保留原系统的多种实现 (文件、Excel、二进制等),由服务器统一管理。
  3. 服务器采用主线程监听连接,每接收一个客户端连接就创建一个新的处理线程,实现多客户端并发访问。

2. 关键问题

主要回答如下问题:

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

3. 运行截图

1、添加学生

2、删除学生

3、查看所有学生

4、按姓名搜索学生

5、按专业搜索学生

(匹配到数据)

(未匹配到数据)

6、按 GPA 搜索学生

7、修改学生信息

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

补充:服务器(以 NetAssist 为例),对于每个合法请求都要按操作类型返回对应响应:

  1. ADD/DELETE/UPDATE 操作:成功时返回 SUCCESS|0|(因为这些操作不需要返回数据,数据长度为 0);失败时返回 FAIL|0|错误原因(比如 "未找到学生")。
  2. 查询类操作(GET_ALL/SEARCH_NAME 等):成功且有数据时返回 SUCCESS|数据长度|数据内容(比如SUCCESS|30|12,kyungsoo,music,5.0,23,male);无数据时返回 NO_DATA|0|。
  3. 错误情况(导致客户端卡住)
    如果服务器收到请求后没返回任何内容,或返回的内容不符合[状态]|数据长度|数据内容格式,客户端的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 客户端与服务器端通信(以某一个具体功能说明)

以"添加学生"为例:

  1. 客户端流程:

    • 用户在控制台输入学生信息(学号12、姓名 kyungsoo 等);
    • NetworkStudentService 将信息拼接为协议格式的请求字符串:ADD|12|kyungsoo|music|5.0|23|male;
    • TCPClient 通过 PrintWriter.println() 发送请求(自动添加 \n );
    • 客户端阻塞等待服务器响应,接收后解析状态(SUCCESS/FAIL)并显示结果。
  2. 服务器端流程:

    • TCPServer 监听8888端口,接收客户端连接后创建 ClientHandler 线程;
    • ClientHandler 通过 BufferedReader.readLine() 读取请求字符串;
    • 按 | 分割字符串,解析操作类型(ADD)和参数,调用 StudentService.addStudent() 处理业务;
    • 处理完成后,返回协议格式的响应: SUCCESS|0| ;
    • 通过 PrintWriter.println() 发送响应(自动添加 \n ),客户端接收后继续执行。

4.2 服务器端将从 Socket 读取到的数据存入本地

服务器端数据持久化流程:

服务器端接收客户端请求后,通过 "Service→DAO" 的调用链将数据存入本地存储(以 TextFileStudentDAOImpl 为例):

  1. ClientHandler 解析请求参数,构建 Student 对象;
  2. 调用 StudentService.addStudent() ,将对象传递给 TextFileStudentDAOImpl ;
  3. TextFileStudentDAOImpl 读取本地文件(如 students.txt )中的现有数据,添加新学生对象;
  4. 通过 BufferedWriter 将更新后的学生列表覆盖写入文件,完成数据持久化;
  5. 若使用 ListStudentDAOImpl ,则数据暂存于内存List中(程序重启后丢失),其他DAO实现(Excel/Binary)流程类似,仅存储格式不同。

4.3 服务器端如何支持多个客户端同时访问

多客户端并发访问实现

  1. 核心设计:主线程监听+子线程处理

    • TCPServer 的 start() 方法中, ServerSocket.accept() 阻塞等待客户端连接;
    • 每接收一个连接,创建一个独立的 ClientHandler 线程(实现 Runnable 接口),专门处理该客户端的所有请求;
    • 主线程继续监听新连接,子线程并行处理已有客户端的请求,实现多客户端并发。
  2. 并发安全保障:

    • 所有客户端共享 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端口。解决方法:

  1. 按下 Win+R 打开运行窗口,输入 cmd 打开命令提示符。
  2. 输入命令netstat -ano | findstr :8080,执行后会显示占用 8080 端口的进程 PID,比如输出结果最后一列的数字 4567 就是对应的 PID。
  3. 可输入tasklist | findstr PID(将 4567 替换为实际 PID)确认进程名称。
    输入命令taskkill /PID 4567 /F(替换 PID 为实际数值),即可强制终止该进程,释放 8080 端口。也能按下 Ctrl+Shift+Esc 打开任务管理器,在 "详细信息" 栏找到对应 PID 的进程,右键选择 "结束任务"。

6. 总结

6.1 学到的知识

  1. 理解C/S架构的核心思想:客户端负责交互,服务器端负责业务逻辑和数据存储,通过网络协议实现通信;
  2. 掌握TCP Socket网络编程:基于字符流的客户端/服务器通信实现,消息边界识别(换行符),多线程并发处理;
  3. 自定义通信协议设计:通过字符串分割格式定义请求/响应,实现标准化数据交换;
  4. 并发安全处理:使用 synchronized 关键字保证共享数据操作的原子性,避免并发冲突;
  5. 问题排查能力:解决端口占用、网络阻塞、数据持久化失败等实际问题。

6.2 改进方向

  1. 协议优化:当前使用自定义文本协议,可替换为成熟的应用层协议(如HTTP、JSON-RPC),简化数据序列化/反序列化逻辑,提高兼容性;
  2. 数据持久化优化:替换文件存储为数据库(如MySQL),支持更复杂的查询和事务,避免文件损坏风险;
  3. 并发性能优化:使用线程池替代手动创建线程,减少线程创建销毁的开销,支持更多客户端并发;
  4. 异常处理增强:添加客户端重连机制、服务器请求超时处理、数据校验(如学号唯一),提高系统稳定性;
  5. 界面优化:将控制台客户端改为GUI客户端(复用原有 MainFrame 、 AddStudentDialog 等类),提升用户体验。

6.3 关于应用层协议改写的思考(结合 AI 建议)

当前系统使用自定义文本协议,虽简单易实现,但存在扩展性差、兼容性弱的问题。若用应用层协议改写,推荐以下方案:

  1. 使用HTTP协议:客户端通过HTTP请求(GET/POST)发送操作指令,服务器端使用Spring Boot搭建HTTP接口,返回JSON格式数据;
    • 优势:通用性强,支持跨平台(如前端页面作为客户端),有成熟的框架支持(Spring Boot、OkHttp);
    • 实现要点:服务器端定义接口(如 /student/add 、 /student/getAll ),客户端用 OkHttp 发送请求,解析JSON响应。
  2. 使用JSON格式优化自定义协议:保留TCP通信,将请求 / 响应数据改为JSON格式(如 {"operation":"ADD","data":{"id":"12","name":"kyungsoo"}} ),使用Jackson库序列化 / 反序列化;
    • 优势:支持复杂数据结构,可读性强,比字符串分割更易维护;
    • 实现要点:实体类添加Jackson注解,服务器端接收JSON字符串后反序列化为对象,处理后序列化返回。
相关推荐
步步为营DotNet1 小时前
深度解析C# 11的Required成员:编译期验证保障数据完整性
java·前端·c#
痕忆丶1 小时前
双线性插值缩放算法详解
算法
万邦科技Lafite2 小时前
一键获取淘宝关键词商品信息指南
开发语言·数据库·python·商品信息·开放api·电商开放平台
fqbqrr2 小时前
2512C++,clangd支持模块
开发语言·c++
han_hanker2 小时前
泛型的基本语法
java·开发语言
Jurio.2 小时前
Python Ray 分布式计算应用
linux·开发语言·python·深度学习·机器学习
vx_bisheyuange2 小时前
基于SpringBoot的社区养老服务系统
java·spring boot·后端·毕业设计
廋到被风吹走2 小时前
【Java】Exception 异常体系解析 从原理到实践
java·开发语言
谷哥的小弟2 小时前
Spring Framework源码解析——GenericTypeResolver
java·spring·源码