ARTICLE DETAIL

资讯详情

深耕网站建设与运营推广的一线实战洞察。

Java局域网聊天室系统全解析:从Socket通信到源码与论文

Java局域网聊天室系统全解析:从Socket通信到源码与论文 简介这是一份面向高校计算机相关专业毕业设计与课程设计的JAVA局域网聊天室系统完整资料包适合需要进行Socket编程、多线程通信或网络应用开发实战训练的学生参考。内容包含可运行源代码与配套毕业论文覆盖聊天室的核心功能设计与实现思路可支撑从选题、编码实现到论文撰写全流程。资源共239个文件包含cpp、h等程序源码文件obj、sbr等编译中间文件另有doc、txt说明文档以及wav音频、ico图标等辅助素材压缩包整体约14.13MB结构清晰便于按需使用。目前已有107人学习下载可作为毕业设计或课程设计的直接参考方案也能帮助快速理解局域网通信的技术要点与项目组织方式。1. JAVA基于局域网的聊天室系统从源码到论文一次讲透做毕业设计或课程设计时局域网聊天室是Java方向最经典的选题之一。它不像电商系统那样引入一堆框架让人头晕又能把Socket通信、多线程、Swing界面这几块硬通货全练到。这套基于局域网的聊天室系统自带完整源代码和论文正好卡在课程设计和毕业设计的交叉点。我拆这套资源的经验是先看架构再看代码最后通电跑起来——三个步骤走完你就能判断它适不适合你以及怎么把它变成你自己的东西。对正在赶工期的学生来说这份资源最大的价值在于闭环。源码是能编译的论文是跟代码对得上的你不用花时间在Word里补图和代码段。对想练手的开发者来说它就是一个干净的局域网聊天室案例服务端与客户端怎么分工、消息怎么转发、在线列表怎么维护看一遍源码就清楚了。下面我按一条真实可复现的路径来拆它。2. 架构与选型为什么局域网聊天室用Java Socket做最稳2.1 C/S结构是聊天室系统的默认答案聊天室本质上是一个多客户端连接单一服务端的场景。用户在客户端输入消息服务端接收后广播给其他客户端整个过程不需要网页浏览器参与。这种场景用C/S客户端/服务端结构写起来最直接因为Java标准库里的java.net.Socket和java.net.ServerSocket就是为它设计的。对比一下B/S结构如果要做一个网页版聊天室你得引入WebSocket、前端框架、消息推送复杂度上了一个台阶。对课程设计和毕业设计来说C/S结构能让你把评审老师的注意力集中在通信逻辑上而不会被前后端联调分散。还有一个隐性原因C/S结构天然契合局域网。同一间机房或同一台路由器下的机器用私有IP就能互连不依赖公网服务器。你们学校机房如果断外网局域网聊天室反而跑得比在线聊天工具还顺畅答辩现场不容易翻车。2.2 TCP优先于UDP聊天室消息不许丢聊天室的通信协议选型上TCP是更稳妥的答案。UDP虽然快但它是无连接、不保证顺序的协议消息发出去可能丢包、乱序。你想想答辩演示的时候老师发一句“大家好”结果消息丢了或者乱序到达客户端显示成“好大家”那场面就尴尬了。TCP的可靠性体现在三个方面建立连接需要三次握手确保客户端和服务端准备好通信发送的数据带序号接收方能重排接收方确认收到后发送方才会丢弃缓冲。对聊天室这种消息频率不高的场景TCP的延迟完全可接受换来的是消息不丢、顺序不乱。在源码里你会发现服务端就是ServerSocket监听一个固定端口比如8888或9999每来一个客户端就分配一个线程处理。客户端用Socket连接到服务端的IP和端口。Java标准库把这套流程封装得很干净写起来基本就是模板式的十几行代码。2.3 多线程模型每客户端一线程简单不容易出错服务端同时要服务多个客户端必须用多线程。最直观的模型是ServerSocket.accept()每接受一个连接就new一个Thread出来把这个连接的Socket交给这个线程去读写。每条聊天消息、每个上下线通知都靠这些线程协作完成。为什么不用NIO或NettyNIO的Selector模型虽然能支撑高并发但对一个局域网聊天室来说是杀鸡用牛刀而且代码可读性差论文里不好讲清楚。线程池模型ExecutorService也可以用但课设和毕设答辩阶段面试老师和评委更期望看到你用原生线程因为这说明你懂线程的创建、运行和销毁。源码里如果用的是原生Thread启动那就跟选型思路完全对上了。下面这张表总结了一下选型对比方便你写论文的技术选型章节时直接参考。技术点本系统方案常见替代方案选择理由网络协议TCPUDP消息可靠顺序到达服务模型每客户端一线程NIO / Netty简单直观适合教学场景客户端UIJava SwingJavaFX / 控制台Swing是Java课必教内容数据结构在线用户集合数据库内存存储够用局域网用户量不大消息格式自定义字符串协议JSON/XML解析简单TCP粘包概率低2.4 消息协议设计用一个分隔符区分消息类型聊天室里不止有聊天消息还有用户上线通知、下线通知、在线列表同步。源码里最通用的做法是设计一套简易文本协议每条消息以特定前缀开头再搭配分隔符分隔参数。常见做法是LOGIN|zhangsan MSG|zhangsan|大家好 LOGOUT|zhangsanLOGIN表示用户名注册MSG表示聊天内容LOGOUT表示退出。服务端收到后根据类型做不同处理。这种协议简单到只需String.startsWith()和String.split()就能完成解析不需要引入JSON库对课程设计来说已经足够。我自己写这类系统时一般会多留一个HEARTBEAT类型做心跳检测防止客户端非正常退出后服务端还留着僵尸用户。不过源码里如果没有这个机制你也可以在论文的“进一步改进”部分提它后面我会详细说怎么加。3. 核心代码这样拆服务端广播与客户端收发的真相3.1 服务端启动ServerSocket与端口绑定的细节服务端的入口代码结构一般是先建ServerSocket然后进入一个无限循环调用accept()每接受一个Socket就起一个线程。这里有几个参数值得注意端口号要选1024以上避免与系统服务冲突backlog参数可以根据预期并发数设置。public class ChatServer { private static final int PORT 8888; // 保存所有客户端的输出流用于消息广播 private static MapString, PrintWriter clients new ConcurrentHashMap(); public static void main(String[] args) throws IOException { ServerSocket serverSocket new ServerSocket(PORT); System.out.println(聊天服务器启动监听端口: PORT); while (true) { Socket socket serverSocket.accept(); // 阻塞等待客户端连接 // 每来一个客户端启动一个独立线程处理 new Thread(new ClientHandler(socket)).start(); } } }ConcurrentHashMap是这里的关键选择。多个线程同时往Map里放客户端输出流时如果用的是普通HashMap并发写入会引发死循环或数据丢失这是生产环境中非常经典的并发事故。用ConcurrentHashMap做线程安全的在线用户表是这段代码里最值得在论文里写一笔的地方。accept()方法是阻塞的在没有客户端连接时线程会停在这里不消耗CPU。当有连接进来时它返回一个已连接的Socket对象后续对这个Socket的读写就是和对应客户端通信。3.2 客户端处理类读消息、广播、响应下线每个客户端连接被new Thread(new ClientHandler(socket))拉起来之后ClientHandler要干三件事读取客户端发来的数据、把数据广播给所有在线客户端、处理客户端断开连接的情况。public class ClientHandler implements Runnable { private Socket socket; private String username; private BufferedReader in; private PrintWriter out; public ClientHandler(Socket socket) { this.socket socket; } Override public void run() { try { // 读取客户端消息 in new BufferedReader(new InputStreamReader(socket.getInputStream(), UTF-8)); out new PrintWriter(socket.getOutputStream(), true); // 第一条消息是登录名注册用户并广播上线通知 username in.readLine(); ChatServer.clients.put(username, out); broadcast(SYSTEM| username 加入了聊天室); broadcast(USERLIST| String.join(,, ChatServer.clients.keySet())); // 循环读取后续消息并广播 String message; while ((message in.readLine()) ! null) { broadcast(MSG| username | message); } } catch (IOException e) { e.printStackTrace(); } finally { // 客户端断开从在线表移除并广播下线通知 ChatServer.clients.remove(username); broadcast(SYSTEM| username 离开了聊天室); try { socket.close(); } catch (IOException e) { e.printStackTrace(); } } } private void broadcast(String message) { for (PrintWriter writer : ChatServer.clients.values()) { writer.println(message); } } }PrintWriter构造函数的第二个参数autoFlush设为true表示每次println后自动刷新缓冲区这样消息能立即发送出去。如果设为false消息会攒在缓冲区客户端迟迟收不到——这是新手最容易忽略的细节后面在避坑章节我会再强调。finally块里处理断开逻辑异常重要。用户直接关窗口时服务端的readLine()会返回null并抛出异常如果不在finally里移除用户名这个用户就会变成永远在线的“幽灵”新用户登录时还会提示重名。3.3 客户端界面与收发线程Swing的多线程约束客户端这边有两件事并行发生用户在输入框打字发送接收服务端广播并显示到聊天记录区。Swing界面是单线程模型所有UI更新必须在事件分发线程Event Dispatch Thread, EDT上执行直接在子线程里调用jTextArea.append()会导致界面卡死或线程安全问题。public class ChatClient { private JTextArea chatArea; private JTextField inputField; private PrintWriter out; private BufferedReader in; public ChatClient(String host, int port, String username) { try { Socket socket new Socket(host, port); out new PrintWriter(socket.getOutputStream(), true); in new BufferedReader(new InputStreamReader(socket.getInputStream(), UTF-8)); out.println(username); // 接收线程持续读取服务端消息更新到聊天区域 new Thread(() - { String line; try { while ((line in.readLine()) ! null) { String finalLine line; // 回调到EDT线程更新UI SwingUtilities.invokeLater(() - chatArea.append(finalLine \n)); } } catch (IOException e) { e.printStackTrace(); } }).start(); } catch (IOException e) { e.printStackTrace(); } } // 发送按钮点击时调用往服务端写消息 private void sendMessage() { String content inputField.getText().trim(); if (!content.isEmpty()) { out.println(content); inputField.setText(); } } }SwingUtilities.invokeLater()把UI更新操作排队到EDT上程序里所有界面刷新都走这一条路。不理解这行代码的话你会在“客户端连接后界面无响应”这个问题上浪费大量时间。客户端的消息解析逻辑就是把服务端发来的字符串按|切分MSG类型取发言人和内容拼接显示USERLIST类型刷新在线用户列表SYSTEM类型直接显示系统通知。这里有个边界情况如果聊天内容本身包含|字符会在切分时出问题。源码一般不会处理这种细节但你在论文的创新点部分提一笔做一个转义或改用首段分隔能成为加分项。3.4 核心流程串起来把服务端和客户端放一起看整个系统的运行逻辑是客户端启动后连接服务端发送用户名作为注册消息 → 服务端把用户名和输出流加入在线表广播上线通知和最新用户列表 → 客户端A发送聊天消息 → 服务端读取到MSG|zhangsan|大家好解析后广播给所有在线客户端 → 客户端B收到消息显示到聊天区 → 客户端关闭窗口服务端finally块清理用户和连接。这个流程就是系统论文里时序图画的逻辑。写论文时把这段描述换成专业术语再配上源码里的类名和方法名评审老师一眼就能看出你是真读过代码的。4. 通电运行从JDK环境配置到局域网互连实战4.1 环境准备JDK版本与编码的三件套检查跑这套系统之前先把环境配好。这里有一个容易被忽略的坑项目可能是在旧版JDK比如JDK 1.6或1.7下写的如果你本机装的是JDK 17甚至更高版本直接编译会报错或警告。老代码里如果有Vector、Hashtable这些集合类新JDK依然兼容但如果用了sun.misc.BASE64Encoder这类内部API高版本JDK就编译不过了。# 检查JDK版本确认是否满足源码要求 java -version # 如果你需要旧版JDK用开源版本或官方历史版本 # 下载后配置JAVA_HOME环境变量 export JAVA_HOME/path/to/jdk1.8.0_202 export PATH$JAVA_HOME/bin:$PATH # 验证配置生效 javac -version我一般习惯把编译环境统一成JDK 8即1.8。它是兼容性最好的版本既能编译老项目也不至于太陈旧让人诟病。Windows上配置JAVA_HOME时要确保路径里没有中文和空格很多编译失败的玄学问题都出在这两点上。源码文件如果是从压缩包里直接解压出来的要注意文件编码。项目里源码如果用的是GBK编码而你的IDE默认UTF-8打开就是满屏乱码。编译时可以通过-encoding参数指定# 编译时显式指定编码防止中文注释和字符串乱码 javac -encoding GBK src/com/chat/*.java -d bin如果没有指定编码Windows中文系统上javac会拿系统默认编码去读源码读到UTF-8的源码时中文注释会变成乱码。更稳的做法是先看源码里中文是不是正常显示再决定用哪种编码编译。4.2 编译与启动先服务端后客户端的顺序不能错源码解压后一般会有src目录源码、bin目录编译产物可能已存在以及一份论文文档。若没有现成编译好的class文件你需要手动编译并启动。命令大致如下# 进入源码根目录 cd chatroom # 编译整个src目录下的所有java文件 javac -d bin src/com/chat/ChatServer.java src/com/chat/ChatClient.java src/com/chat/ClientHandler.java # 启动服务端 java -cp bin com.chat.ChatServer # 另开两个终端窗口分别启动两个客户端 java -cp bin com.chat.ChatClient java -cp bin com.chat.ChatClient启动顺序有讲究服务端必须最先启动如果客户端先启动它会因为连不上ServerSocket而直接抛出ConnectException。启动服务端后终端会打印出监听端口确认无报错后再启动客户端。客户端启动后通常会弹出一个登录界面让你输入用户名和服务器IP。如果是本机测试服务器IP填127.0.0.1或localhost即可。如果你在服务端打印一下本机IP用ipconfigWindows或ifconfigLinux/Mac查看局域网IP填进去那就说明已经开始真正走局域网通信了。4.3 局域网互连三要素IP、防火墙、同一网段在教室或宿舍里测试时两个客户端要分别放在两台电脑上跑。这时有几个前置条件必须满足缺一个都连不上检查项操作失败时的典型症状IP连通性客户端电脑ping服务端IP请求超时说明不在同一网段或物理隔离端口开放服务端电脑防火墙放行8888端口客户端报Connection refusedIP正确性客户端填写服务端的局域网IP而非127.0.0.1连接被拒绝或超时防火墙是最大的坑。Windows系统默认会拦截对未授权端口的入站连接服务端程序监听8888端口时Windows会弹窗询问是否允许很多人直接点了“取消”。等客户端连不上时第一反应是代码写错了折腾半天才发现是防火墙问题。# Windows命令行添加防火墙入站规则需要管理员权限 netsh advfirewall firewall add rule nameChatServer8888 dirin actionallow protocolTCP localport8888这条命令在Windows 10和Windows 11上都有效。Linux服务器上如果有iptables或firewalld开启也要放行对应端口。这里的原则是先确认IP能不能ping通再看端口通不通。用telnet测试最快telnet 192.168.1.100 8888能看到光标停留在空白状态就说明端口开放了。同一网段这个条件也常被忽略。教室无线网络可能划分了多个VLAN学生的电脑和老师的电脑不在一个网段时即使物理距离很近网络层也不互通。判断方法很简单两台电脑的IP地址前三段是否相同比如192.168.1.x不同就是跨网段了需要路由器同网段的另一台机器中转。4.4 在单机上模拟多用户localhost验证心法没有第二台电脑的时候可以在一台机器上开多个客户端窗口来验证系统功能。因为localhost会回环到本机的ServerSocket所以多少个客户端都能连接到同一个服务端。第一步先跑通单机测试确认功能完备后再上局域网这样能有效缩小排查范围。# 终端1启动服务端 java -cp bin com.chat.ChatServer # 终端2启动客户端A java -cp bin com.chat.ChatClient # 终端3启动客户端B java -cp bin com.chat.ChatClient如果单机多客户端一切正常但局域网连接有问题那八成是IP或防火墙的事代码本身不用怀疑。这套排查思路能帮你省掉大量时间。5. 避坑指南源码复现时最容易翻车的五个真实问题5.1 端口被占用服务端启动直接抛BindException现象运行java -cp bin com.chat.ChatServer时控制台抛出java.net.BindException: Address already in use: JVM_Bind服务端直接退出。原因上一次运行服务端时没有正常关闭或者系统里已经有另一个程序占用了8888端口。Windows下非常常见程序崩溃后Socket没释放端口处于TIME_WAIT状态。解决先确认谁占用了端口再杀掉对应进程。Windows下用netstat -ano | findstr 8888找到PID然后taskkill /F /PID 进程号。也可以直接把服务端端口改到其他值比如换成9999改完后客户端连接时也要同步修改端口。这段血泪经验是我自己踩过的我之前在一台机器上同时跑两个项目端口冲突找了一个小时才反应过来。5.2 中文乱码网聊变成了火星文现象客户端收到的聊天消息里中文全部变成乱码服务端的系统通知也是乱码。原因服务端和客户端之间的IO流编码不一致。源码里如果用了InputStreamReader(socket.getInputStream())没指定字符集就会用系统默认编码Windows中文系统通常是GBK。客户端发的是UTF-8服务端用GBK读中文自然乱。解决在两端都显式指定相同的字符集。把读取流改成new BufferedReader(new InputStreamReader(socket.getInputStream(), UTF-8));同时写入端也要统一。服务端PrintWriter输出时用new PrintWriter(new OutputStreamWriter(socket.getOutputStream(), UTF-8), true)客户端对应位置一样改。这样只要两端编码统一乱码立刻消失。审核源码时如果所有流都用了统一的Charset常量就说明作者注意到了这个问题。5.3 客户端关闭后服务端用户列表残留僵尸用户永不消失现象A用户关闭聊天界面但其他用户的在线列表里仍然显示A且A的名字无法再登录提示“用户名已存在”。原因ClientHandler的run()方法里正常读完消息后循环退出但断开清理逻辑放在了catch或finally里如果用户是直接点窗口右上角关闭Socket断开时服务端可能处于阻塞readLine()状态抛出的异常没能触发正确的清理路径。解决检查ClientHandler的finally块确保清理逻辑一定执行finally { if (username ! null) { ChatServer.clients.remove(username); chatArea.broadcast(SYSTEM| username 离开了聊天室); } socket.close(); }还有一个补充机制客户端关闭窗口时主动发一条LOGOUT消息服务端收到后立即清理。合理做法是把服务端主动检测和客户端主动通知两条路径都写上双保险。5.4 编译报错“编码GBK的不可映射字符”现象在Windows命令行运行javac编译源码时报大量错误: 编码GBK的不可映射字符 (0x...)。原因源码文件是UTF-8编码系统javac却用GBK去读。这个错在Java老项目里非常常见。解决编译时加上-encoding utf-8参数。如果你不确定源码是什么编码用Notepad或VS Code打开看右下角编码标识。如果源码是GBK就写-encoding gbk是UTF-8就写-encoding utf-8。关键是两端对齐。解决Deep dive如果你用的是IDE而不是命令行要注意IDE默认编码设置。比如IntelliJ IDEA里Settings → Editor → File Encodings全部改为UTF-8再重新编译。5.5 程序能连上但消息发不出来autoFlush未开启现象客户端连上服务端不报错但发消息石沉大海服务端也收不到任何消息。原因PrintWriter构造时没有开自动刷新println进去的消息攒在缓冲区不发送要有后续写入才会跟着一起走。这是我调试时最容易忽视的一环正常情况下消息的延迟会给人一种“网络卡了”的错觉。解决new PrintWriter(socket.getOutputStream(), true)第二个参数true就是打开autoFlush。或者每次写入后手动调用out.flush()。但强烈建议直接开autoFlush因为每行写完自动刷既不用手动调flush()也不会漏刷。6. 拿来后怎么改四个让它从课设变成优秀设计的改进方向6.1 加心跳检测让在线列表不再靠玄学源码里的在线状态机制用户非正常退出时会在服务端残留这是前面避坑章节提到的老毛病。在一份能拿到不错的课设评价的源码里通常都会有心跳机制。实现起来很简单客户端每5秒发一条HEARTBEAT消息服务端记录每个用户最后活跃时间每隔10秒扫描一次超过15秒没活跃就踢掉。// 服务端心跳扫描线程 public void startHeartBeatCheck() { ScheduledExecutorService scheduler Executors.newScheduledThreadPool(1); scheduler.scheduleAtFixedRate(() - { long now System.currentTimeMillis(); ChatServer.clients.forEach((username, lastHeartBeat) - { if (now - lastHeartBeat 15000) { // 超过15秒没心跳判定离线 broadcast(SYSTEM| username 掉线了); ChatServer.clients.remove(username); } }); }, 10, 10, TimeUnit.SECONDS); }ScheduledExecutorService比Timer好在哪里Timer如果任务执行抛出异常整个调度线程就挂了ScheduledExecutorService会捕获异常并继续下一个周期。这个细节在论文里写一笔能显得你关注线程池的健壮性。心跳间隔和超时时间不是随手选的。间隔太短会浪费CPU和带宽太长则在线状态不实时。局域网环境下5秒心跳、15秒超时是比较合理的值既不影响性能又能在一分钟内发现断线。6.2 私聊实现一套协议走天下源码里如果只有群聊你可以给它加私聊功能。协议扩展很直接PRIVATE|接收者|发送者|消息内容服务端在广播前检查消息类型是PRIVATE时就只发给指定客户端if (message.startsWith(PRIVATE)) { String[] parts message.split(\\|); String targetUser parts[1]; String sender parts[2]; String content parts[3]; PrintWriter targetOut ChatServer.clients.get(targetUser); if (targetOut ! null) { targetOut.println(PRIVATE| sender | content); } else { // 目标用户不在线返回提示 ChatServer.clients.get(sender).println(SYSTEM|用户 targetUser 不在线); } }这里有一个正则细节String.split(\\|)必须转义竖线因为split接收的是正则表达式而|是正则里的“或”操作符。直接用split(|)会把每个字符都拆开这是网上聊天室源码里非常高频的翻车点。私聊加完之后客户端界面还要做区分。一般是双击在线列表里的用户名自动在输入框前带上提示或者弹一个独立的小窗口。如果只改协议不改UI私聊功能就是半残的答辩演示时会很尴尬。6.3 聊天记录落库给答辩加一个数据库亮点局域网聊天室纯内存运行关闭服务端聊天记录就没了。如果你论文里写了数据库相关的内容但系统里没体现等于白写。给聊天记录加持久化不需要引入完整数据库用SQLite或JDBC连接MySQL都行// 消息落库的DAO层 public class MessageDao { private Connection conn; public void saveMessage(String sender, String content, Timestamp time) { String sql INSERT INTO chat_message (sender, content, time) VALUES (?, ?, ?); try (PreparedStatement ps conn.prepareStatement(sql)) { ps.setString(1, sender); ps.setString(2, content); ps.setTimestamp(3, time); ps.executeUpdate(); } catch (SQLException e) { e.printStackTrace(); } } }用PreparedStatement而不是Statement拼接SQL既防SQL注入又省去每次拼接字符串的转义麻烦。论文里写“使用预编译语句防范注入攻击”这一句话就能展示安全素养。表结构不用复杂id自增主键sender发送人content消息内容time时间戳。加一个is_private字段区分群聊和私聊消息也行。答辩时你从聊天记录里翻出一条昨天的消息比口头描述系统多强大都有说服力。6.4 文件传输通道图片发送不再纸上谈兵聊天室只发文本总感觉差点意思。加文件传输的思路是用TCP单独的socket连接传文件聊天线程不受阻塞。客户端选择文件后先发一条FILE|文件名|文件大小的元数据消息然后建立数据通道流式写入。// 发送文件的核心方法 public void sendFile(Socket socket, File file) { byte[] buffer new byte[4096]; try (FileInputStream fis new FileInputStream(file); BufferedOutputStream bos new BufferedOutputStream(socket.getOutputStream())) { int bytesRead; while ((bytesRead fis.read(buffer)) ! -1) { bos.write(buffer, 0, bytesRead); bos.flush(); } } catch (IOException e) { e.printStackTrace(); } }缓冲区大小4096字节是一个常见的折中值太小则读盘次数多太大则占用内存多且可能压制流式读取效率。实际传文件时4KB的缓冲区传输速率基本能跑满局域网带宽。文件流的flush()要放在循环里而不是循环结束后一次性刷新。如果只在最后刷一次接收端可能还没读完缓冲区就等不到后续数据导致传输中断。6.5 第一人称收尾俺的打包复查流水线自从有一次答辩前夜发现源码里某处PrintWriter没开自动刷新导致现场演示翻车之后我就养成了一个习惯每次拿到这种课设毕设资源先过三样——JDK版本对不对、编码统不统一、重启之后还能不能跑。然后在此基础上按上面的顺序把心跳、私聊、落库、文件传输四个改进按需挑着加。这套组合拳打下来一份“能跑”的课设就升级成了一份“讲得深、改得起”的毕设。希望帮到你赶紧把环境配起来吧。本文还有配套的精品资源点击获取
返回列表