基于Java Socket多人在线聊天系统:从原理到实战完整设计
要说Java网络编程里哪个项目最经典十个有九个会给你推荐Socket聊天室。这东西看起来简单无非就是ServerSocket在上面监听、Socket在下面读写但等你真把一个多人在线聊天系统完整跑起来从线程模型到消息协议再到并发安全一个小时就能让你把网络上那些“八股文”全部落到实处。我自己当年入门Java后端时也是拿这个项目练手后来帮别人改过不少类似的代码发现大多数人卡在同一个地方不是不会写ServerSocket而是不知道连接建立之后该怎么管、消息该怎么转、客户端怎么不掉线。这篇博文就基于我们常说的“基于 Java Socket 实现多人在线聊天系统”项目把完整的设计思路、核心源码、运行步骤和踩坑记录全部捋一遍。不管你是刚学完Java SE、想找个项目练手的在校生还是工作中需要快速搞一个内网协作工具、临时通知小系统的开发者这个项目都能满足你在“最短时间内学会网络编程落地”的需求。我会把代码按模块拆开讲每一段都告诉你为什么要这么写而不是单纯贴一段代码让你抄。1. 项目整体回顾与设计思路1.1 为什么选 Socket 而不是 Netty 或 WebSocket很多人一听到“聊天系统”就下意识想到Netty、WebSocket其实这两个选型在这个项目里都不太合适。Netty虽然性能强劲但它的异步模型和EventLoop机制对新手来说是一个很陡的坡你大概率会在处理ChannelHandler时被回调绕到怀疑人生WebSocket则是基于HTTP协议升级来的适合浏览器端实时通信如果你纯粹为了练Java网络编程基础它反而混淆了TCP协议的学习主线。Socket编程是Java网络通信的底层基础它直接暴露TCP连接的建立、发送、接收全过程。你写Socket聊天室时能肉眼看到客户端与服务端之间的连接是长连接还是短连接、数据是怎么按字节流进出的、多客户端之间为什么需要转发。这些底层机制理解透了之后去看Netty的Channel、Pipeline、EventLoop你会发现全都是Socket那套东西的工程化封装。项目的目标是学原理、打基础Socket就是最合适的选择。1.2 核心功能规划与需求拆解一个标准的多人在线聊天系统最少需要满足这么几个需求多个客户端可以同时连接同一个服务端且互不影响任何一个客户端发送消息服务端都要把消息广播给所有在线用户新用户上线、老用户下线系统能及时告知其他人服务端能记录在线用户列表支持查询当前有哪些人在线客户端可以主动断开连接且服务端回收对应的资源不发生内存泄漏。你别看功能好像不多这里面每一块都对应一个网络编程的核心考点。第一个需求对应“多线程处理连接”第二个对应“消息转发机制”第三个对应“连接状态管理”第四个对应“在线用户维护”第五个对应“资源清理”。把这五件事做明白你就能在面试里把TCP通信、多线程、集合类这几个高频知识点串成一条线来讲绝对比背一百道题都有说服力。1.3 技术选型与整体方案站在项目架构角度我会把系统拆成服务端和客户端两端。服务端用ServerSocket监听固定端口每接收一个连接就创建一个线程去处理该连接客户端用Socket连接服务端地址主线程负责读取用户输入另一个线程负责接收服务端转发来的消息。通信协议上第一版我会选择自定义的纯文本协议用分隔符把消息头和消息体切开。为什么不用JSON道理很简单JSON解析在你做完整项目时很有必要但在这个练手项目里增加解析库会让通信逻辑被第三方依赖占据反而不利于你聚焦Socket本身。等核心功能跑通后你再把协议换成JSON你会发现只是换一个编码解码函数而已整个通信骨架完全不受影响。这也是一个很重要的架构思想通信协议要独立于业务逻辑方便后续替换升级。2. 系统架构与模块划分2.1 客户端-服务端两层结构明确一点这个项目不引入中间件不引入数据库就是一个标准的两层结构客户端直接连服务端。服务端是信息的中转站所有消息都先进服务端再由服务端决定是广播给所有人、私聊给某个人还是存下来后面再看。在这个结构里服务端要做的事可以细分为三个模块连接管理模块负责维护所有在线客户端的Socket用户管理模块负责记录用户名和对应连接的映射关系消息处理模块负责读取消息、解析指令、执行转发。客户端则相对简单只需要维护一个Socket做两件事发送用户输入接收服务端消息。2.2 消息协议的设计思路协议是整个聊天系统里最容易被新手忽略的部分。有些人写Socket聊天直接让客户端发字符串、服务端收字符串不定义任何格式。这在小规模Demo里能跑但只要你想加一个“用户上线通知”或“系统消息”就傻眼了客户端怎么区分哪条是普通聊天内容、哪条是上线提醒所以消息必须带类型否则所有消息混在一起客户端根本没法决定渲染方式。我第一版协议这样定义消息类型|发送人|消息内容三段之间用|分隔。第一段是消息类型比如MSG表示聊天消息、SYSTEM表示系统通知、ONLINE表示新用户上线、OFFLINE表示用户下线第二段是发送人名字第三段是消息内容。这条规则写明白后客户端收到任何消息都能准确解析类型再决定是显示成普通对话、还是弹出一行系统提示。协议不需要复杂但边界必须清晰。一个成熟的通信项目一定是从这种极简协议开始的后续扩充版本号、消息ID、时间戳时你会发现只要遵从“类型|发送人|内容”这个骨架扩展起来并不痛苦。2.3 多线程模型的选择与理由线程模型是Socket聊天室能否撑住多个客户端的关键。我的方案是“一个客户端连接对应一个服务端线程”这也是最直观的一种模型。主线程只做一件事死循环执行accept()每来一个连接就建一个线程扔给处理器去跑。这种方式有什么好处首先每个客户端都是独立的Reader线程互不阻塞其次主线程和子线程职责分离谁都不会因为对方忙而卡住。缺点也明显线程是宝贵的系统资源撑几百个连接还行撑几千个就会出问题。但这恰恰是项目的教学价值所在你先用“一连接一线程”把模型跑通然后才能理解Netty的Reactor多线程模型为什么更牛、为什么EventLoop能在一个线程里处理成千上万个Channel。写代码时要注意一点处理线程要用匿名内部类或Lambda开新线程但千万别让主线程直接去处理读消息的逻辑否则第一个客户端的连接会把整个服务端阻塞住其它客户端全都没法上线。这个我在后面还会强调。2.4 在线用户集合与线程安全服务端需要维护一个“在线用户表”最简单的方式是用HashMapString, Socket把用户名和连接Socket对应起来。但这里有个并发隐患多个用户线程可能同时上线、同时下线同时对这张表做读写普通HashMap在并发环境下会被破坏内部结构轻则数据错乱重则直接死循环。所以必须用ConcurrentHashMap。这个类的内部做了分段锁读操作不加锁、写操作只锁对应桶位在多线程读写场景下性能和安全都有保障。我做这个项目时一开始也图省事用了HashMap结果跑五分钟就开始出诡异Bug某个用户下线后别人发消息还能收到他的空回复。排查了半天才想到集合被并发写坏了。这就是标准的“项目教你做人”的案例——你以为知道线程安全这种事真出事时才发现自己是知道但没做到。3. 核心代码实现与关键步骤3.1 服务端的主体代码结构先看服务端入口类和连接接收循环。这是整个系统的“总开关”。public class ChatServer { private static final int PORT 12345; private static ConcurrentHashMapString, Socket onlineUsers 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(); System.out.println(新客户端连接 socket.getRemoteSocketAddress()); new Thread(new ClientHandler(socket)).start(); } } public static ConcurrentHashMapString, Socket getOnlineUsers() { return onlineUsers; } }ServerSocket创建时指定端口然后accept()方法会在没有连接时阻塞等待。每accept()到一个连接就创建一个ClientHandler实例把它丢给新线程去执行。这里有个细节ClientHandler必须实现Runnable接口里面写这个客户端连接的完整生命周期逻辑。为什么不用继承Thread因为Java推荐“组合优于继承”而且这里线程只需要执行一次任务实现Runnable接口更灵活、也更符合主流写法。我见过有人把ClientHandler写成内部类这没问题但把它写成独立类会更清爽方便后续维护和测试。这个看个人习惯了。3.2 ClientHandler服务器的核心处理逻辑这个类是我认为整个项目里最值得反复看的代码因为它把通信、状态管理、资源释放这几件事全部集齐了。public class ClientHandler implements Runnable { private Socket socket; private String username; private BufferedReader reader; private PrintWriter writer; public ClientHandler(Socket socket) { this.socket socket; } Override public void run() { try { reader new BufferedReader(new InputStreamReader(socket.getInputStream())); writer new PrintWriter(socket.getOutputStream(), true); // 第一步读取并注册用户名 username reader.readLine(); ChatServer.getOnlineUsers().put(username, socket); broadcast(SYSTEM|服务器| username 上线了); String message; while ((message reader.readLine()) ! null) { System.out.println(收到消息 message); broadcast(MSG| username | message); } } catch (IOException e) { System.out.println(客户端异常断开 username); } finally { // 清理资源 if (username ! null) { ChatServer.getOnlineUsers().remove(username); broadcast(SYSTEM|服务器| username 下线了); } try { socket.close(); } catch (IOException e) { e.printStackTrace(); } } } private void broadcast(String message) { for (String name : ChatServer.getOnlineUsers().keySet()) { Socket targetSocket ChatServer.getOnlineUsers().get(name); try { PrintWriter targetWriter new PrintWriter(targetSocket.getOutputStream(), true); targetWriter.println(message); } catch (IOException e) { e.printStackTrace(); } } } }这个类的流程很清楚先读一行用户名完成注册然后循环读取客户端发来的内容读到了就广播读到null就说明客户端断开了连接。readLine()返回null是判断TCP连接关闭的标志这一点一定要记住当对方正常调用close()或异常断开时对端的输入流会返回null而不是抛出异常。broadcast方法里有一点需要注意我是遍历在线用户表逐个获取Socket并创建新的PrintWriter发送数据。这种方法能跑但每次广播都要新建很多Writer效率上有优化空间。更合理的做法是在用户上线时就把PrintWriter缓存到对应的用户对象里广播时直接取用。不过第一版为了简化代码逻辑直接循环创建Writer更能表达“转发”这个语义。你理解了之后再自己优化成本地存储Writer就很自然了。3.3 客户端核心代码结构客户端比服务端简单但它同样涉及双线程问题。如果客户端只用一个线程既要读键盘又要读网络一定会卡死在一个阻塞读上。所以我采用“主线程发消息、子线程收消息”的结构。public class ChatClient { public static void main(String[] args) throws IOException { Socket socket new Socket(127.0.0.1, 12345); System.out.println(已连接服务器); BufferedReader console new BufferedReader(new InputStreamReader(System.in)); PrintWriter writer new PrintWriter(socket.getOutputStream(), true); BufferedReader reader new BufferedReader(new InputStreamReader(socket.getInputStream())); System.out.print(请输入用户名); String username console.readLine(); writer.println(username); // 接收消息的线程 new Thread(() - { String message; try { while ((message reader.readLine()) ! null) { System.out.println(message); } } catch (IOException e) { e.printStackTrace(); } }).start(); // 主线程发送消息 String input; while ((input console.readLine()) ! null) { if (exit.equalsIgnoreCase(input)) { writer.println(bye); socket.close(); break; } writer.println(input); } System.exit(0); } }客户端的收消息线程必须单独剥离出来这是无数人会踩的坑如果你把收消息的循环也放在主线程里那么当用户还没在控制台输入任何内容时程序就会阻塞在console.readLine()上服务端发来的任何消息都没法显示。必须保证“读控制台”和“读Socket”是两个完全不互相干扰的动作。System.exit(0)这行是我后来加的。原因很实在当用户输入exit退出主循环后接收消息的子线程可能还阻塞在reader.readLine()上如果不强制退出JVM程序会“看似退出了但进程还在”。这在控制台Demo里体验极差。用System.exit(0)虽然粗暴但在这个场景里反而是最直接有效的办法。3.4 消息协议的定义和解析为了让客户端能正确区分不同类型的消息我们要在客户端定义一个解析方法。按我前面规定的类型|发送人|内容三段式解析逻辑非常简单。private static void handleMessage(String rawMessage) { String[] parts rawMessage.split(\\|, 3); if (parts.length 3) { return; } String type parts[0]; String sender parts[1]; String content parts[2]; switch (type) { case SYSTEM: System.out.println([系统通知] content); break; case MSG: System.out.println(sender content); break; default: System.out.println(rawMessage); } }split(\\|, 3)里面的限制参数3很关键它表示最多分割成3段。这样做是为了防止消息内容里本身就带有|字符导致内容被截断。如果直接split(\\|)那么内容是“你好|最近怎么样”的时候会被拆成4段后面的parts[2]拿到的就不是完整内容了。这是一种很经典的分隔符协议防护策略跟CSV处理时考虑引号是一个道理。到这里为止一个完整的聊天系统已经可以跑起来了。你开一个服务端再开两个客户端分别在两个客户端里输入不同的用户名然后任一边发句话另一边就能收到。这就是最原始也是最本质的多人在线聊天。4. 完整运行指南与实操步骤4.1 环境准备与项目导入运行这个项目环境要求很简单JDK 8以上任意Java IDE不需要任何第三方依赖。如果你用的是Eclipse或IntelliJ IDEA直接新建Java项目把ChatServer.java和ChatClient.java两个类文件丢进src目录就行。IDEA里运行时有一个非常关键的小坑运行ChatClient的时候会启动好几个客户端实例如果你直接点绿色三角按钮IDEA会问你“是否同时启动多个实例”一定要选“允许”否则你只能开一个客户端根本测不了多人在线聊天效果。Eclipse用户可以在Run Configuration里开启“Allow multiple instances”或者干脆复制几次运行配置来解决。服务端和客户端的启动顺序是先运行ChatServer看到控制台输出“服务端启动”再运行多个ChatClient实例。如果先启动客户端客户端会抛出ConnectException: Connection refused因为根本没有服务端在监听这个端口。这个异常是Socket编程里最常见的看到它先别慌检查一下端口能不能通、服务端有没有启动就是第一个排查方向。4.2 控制台交互流程演示假设你现在已经启动了一个服务端、两个客户端。整个过程应该是这样的第一个客户端启动后控制台提示输入用户名输入张三服务端立刻在后台打印“新客户端连接”并广播系统消息“张三上线了”。但注意此刻另一客户端还没上线所以这条广播没有接收者。从第二个客户端输入李四开始双方就能互相通信了。张三发一条大家好服务端收到后拼装成MSG|张三|大家好广播给所有在线客户端。张三自己的客户端也收到了这个广播所以自己发的话自己也会在屏幕上看到。这种“回显”机制很符合日常聊天软件的行为——你在微信里发消息自己聊天框也会显示自己发的消息。如果你不想让他人看到自己的聊天记录可以在广播前增加条件判断不发给发送者本人这是一个常见的简单变体。当张三输入exit退出时服务端检测到连接断开从在线用户表中移除张三并广播“张三下线了”。李四的客户端屏幕上会显示系统通知整个流程清晰明了。4.3 如何验证系统的健壮性跑通基本流程后我建议你额外做几个测试来验证系统的健壮程度。第一个测试是“客户端非正常退出”直接在IDEA里强制终止一个客户端进程不要正常发送exit。这时候服务端会怎么表现理想情况下readLine()返回null走进入finally正常清理用户信息并广播。偶尔你会在服务端控制台看到SocketException: Connection reset异常这是因为对端TCP层直接发来了RST包。我的代码把IOException捕获住并打印日志最终还是会走finally清理所以不会出现僵尸用户。第二个测试是“服务端同时接收大消息”。你可以一次粘贴一大段文字发送看服务端和客户端是否会出现乱码或截断。由于readLine()是按行读取只要你发送的内容里不含换行符一切正常。如果包含换行符就会拆分成多条消息这是一个协议的天然限制也是后面项目升级时可以考虑设计变长数据帧的原因。第三个测试是“多客户端交叉通信”开四个客户端同时互发消息观察是否有消息丢失或串台。只要你是用了ConcurrentHashMap基本不会出现串台问题。5. 避坑记录与常见问题排查5.1 并发集合问题为什么不用 ArrayList有一段时间我的在线用户集合用的是HashMap后来改成ConcurrentHashMap中间踩到了一个非常典型的并发Bug。场景是用户A下线时从集合中移除自己的条目同时用户B上线时向集合中写入自己的条目两个线程同时写HashMap的内部指针被破坏某个正在被遍历的桶位指向了错误节点最终结果是某个客户端的消息被发给了并不存在的Socket抛NullPointerException。解决办法表面上是换个集合类但深层是要理解“写操作之间不能互相干扰”的原则。ConcurrentHashMap在JDK 8之后采用CASsynchronized机制对单个桶位进行并发写时能保证安全而HashMap完全没有这个机制。类似地如果你用CopyOnWriteArrayList存在线列表也是可以的它的原理是每次写都复制一份数组读操作永远不加锁适合读多写少的场景。但在这个项目里在线列表的写操作也挺频繁每次都复制数组代价偏大所以ConcurrentHashMap还是最优解。5.2 粘包问题到底什么时候会遇到很多人一写Socket就紧张“粘包拆包”其实在我的Demo里基本不会遇到。为什么因为BufferedReader的readLine()按行读取TCP层无论怎么粘包只要发送端每写一行就换行符readLine()都会把一行数据从流里单独取出来。真正会遇到粘包的是当你用byte[]缓冲区一次性读N个字节、不做行协议处理的时候。比如你用InputStream.read(byte[])读1024字节那一次可能读到多条消息拼在一起。这也是我特意把协议设计成“一行一条消息”的原因用行的边界作为消息的边界规避了粘包问题。如果你想进一步优化协议可以把\n换行符视为帧结束符发送端在每条消息末尾补一个\n这种做法在很多基础网络库中都很常见。所以你在本项目中不需要特别处理粘包但如果你把协议改成二进制或长度字段式协议就必须自己处理数据帧边界了。5.3 端口占用问题ServerSocket绑定端口时如果端口已经被占用会抛出BindException: Address already in use。这种情况多半是上一次运行服务端时没有正常关闭进程还在后台跑着。Windows上可以直接打开任务管理器关掉Java进程macOS或Linux上可以用lsof -i:12345查出占用端口的PID然后kill掉。另外一个更隐蔽的情况是你关闭服务端主窗口时IDEA并没有完全终止进程Java进程还在后台运行。这是因为原来accept()阻塞在那边main方法没有退出。解决办法是在服务端捕捉关闭信号然后主动关闭ServerSocket释放端口。我在实际使用中为了方便通常直接改端口号来绕过这个问题但正式写项目时还是要优雅关闭。5.4 客户端掉线后的资源回收当一个客户端掉线时服务端最好能立刻感知并清理资源。readLine()返回null是正常断开抛出SocketException是异常断开两种情况我都需要调用socket.close()、把连接从在线表中移除、并广播下线通知。这个“finally清理”的思路在所有的网络编程中都是通用的。我见过有些人把清理逻辑写在catch里表面上看也执行了但万一前面的try块抛了非IOException的异常finally照样能兜底。所以一定让清理代码只出现在finally里不要放在catch块中这样才能保证“无论咋样都回收”。6. 项目扩展方向与升级思考6.1 从单房间到多房间引入房间概念现在的系统是所有人都在一个大公共频道里聊天。如果你想升级成类似QQ群或Discord频道模式只需要增加一个房间概念。服务端维护一个ConcurrentHashMapString, List用户标识每个房间对应一组用户消息只广播给同房间的人。这个扩展看似简单却会带来一系列设计问题用户如何发指令进房、换房服务端怎么通知房间内成员有人进出房间解散时里面的用户怎么处理这些问题的解决过程能让你的代码能力上一个台阶。你甚至可以加一个指令系统用户发送/join 房间名来加入房间发送/leave来退出当前房间所有带/开头的消息都当作指令解析不走普通广播逻辑。这个思路也是很多IM系统命令处理的基础形态。6.2 消息持久化与历史记录现在的系统不存历史消息客户端看不到之前的聊天记录。要加消息持久化最简单的方案是引入一个嵌入式数据库比如H2或SQLite在服务端广播前先把消息写入数据库。这样新用户上线时可以发送一条/history指令服务端从数据库读出最近N条记录回发给客户端。引入持久化之后你会发现一个之前没注意到的点消息格式里的时间戳几乎成了必需品。没有时间戳历史记录显示出来根本不知道是什么时候发的。所以你需要在消息协议里增加一列字段比如MSG|张三|2024-05-20 10:30:00|大家好。这个升级会反过来倒逼你重新设计协议和消息解析代码这其实就是系统演进中很自然的过程。6.3 性能优化线程池替代一连接一线程如果你希望这个聊天系统能支撑更多客户端第一件事就是把“一个连接一个线程”改成“线程池”。使用ExecutorService来管理线程限制了同时存在的线程数量上限降低资源消耗。ExecutorService executor Executors.newFixedThreadPool(20); while (true) { Socket socket serverSocket.accept(); executor.submit(new ClientHandler(socket)); }这样改完之后线程上限是20个如果超过20个客户端连接新连接会被放在任务队列中等待处理。对学习而言等你能说出用线程池相比直接new Thread的三个好处线程复用、限制并发、防止无限制创建导致OOM这个项目的进阶版本就算做得比较扎实了。再往后才是NIO和Netty的世界。你用Socket聊天室打好的所有基础都会在Netty里重新遇到一遍——只不过包装更优雅、性能更好。6.4 全局异常处理与心跳机制最后补一个容易被忽略的现实问题网络连接不一定每次都体面地断开路由器重启、网线被拔、电脑睡眠都会导致TCP连接“假死”——服务端还持有这个Socket但对方其实已经收不到任何消息了。针对这种情况标准的做法是心跳机制客户端每隔一段时间发送一个PING消息服务端如果在规定时间内没收到这个客户端的任何数据就判定它失联了清理掉对应的连接。你可以在消息协议中增加PING|用户名|这个类型客户端每隔30秒自动发送一次服务端收到后不需要广播只需要更新这个用户的最近活跃时间。服务端定期扫描所有用户把超过90秒没有活跃的用户强制下线。这个机制在真实的IM系统里是标配Socket聊天室加上心跳机制后就从一个纯Demo变成一个有生产意识的练习作品了。我自己的体会是这个项目最大的价值不在代码量的多少而是它逼你把“网络通信”这样一个抽象概念落实到一行一行可以调试、可以观察的代码上。你会亲眼看到客户端和服务端之间每一行消息是怎么流转的也会真切地理解为什么线程池、并发集合、心跳机制这些知识点不是在背八股文、而是在解决真实存在的问题。按照这个思路把项目做完你收获的不只是一个“附完整源码”的练手Demo更是一整套网络编程的调试思路和排错手感。之后再上手Netty或者Spring WebSocket你会觉得很从容因为底层的“连接-消息-转发-清理”这套心智模型你已经建起来了。

相关新闻

易语言API模块CreateThread线程创建启动实战详解

易语言API模块CreateThread线程创建启动实战详解

/* MD / 富文本中的 .toc(含博客园搬家等嵌套结构);.toc-box 在侧栏,不受影响 */#content_views .toc,/* 编辑器常在目录前后插入空 p(:empty 仍占 20px),一并去掉避免顶空隙 */#content_views.markdown_views > p:empty:has(+ .toc),#content_views.markdown_views …

2026/10/16 3:50:09 阅读更多 →
Edge 能装 AB Download Manager 扩展吗?下载接管原理与安装入口一次说清

Edge 能装 AB Download Manager 扩展吗?下载接管原理与安装入口一次说清

Edge 能装 AB Download Manager 扩展吗?下载接管原理与安装入口一次说清 【免费下载链接】ab-download-manager A Download Manager that speeds up your downloads 项目地址: https://gitcode.com/GitHub_Trending/ab/ab-download-manager 如果你已经在用 A…

2026/10/16 3:50:09 阅读更多 →
AI辅助论文写作全流程指南:从文献阅读到查重降重

AI辅助论文写作全流程指南:从文献阅读到查重降重

写毕业论文最折磨人的往往不是“写”本身,而是从一片混沌到逻辑成型的过程。选题方向拿不准、文献看了几百篇记不住、提纲改了七八遍还是散、初稿写出来像白开水、查重报告红得扎眼——这些关卡单靠意志力硬扛,效率极低。这两年AI工具的成熟度已经完全不…

2026/10/16 3:49:09 阅读更多 →

最新新闻

FPGA高速安全状态机设计:编码、时序优化与安全机制

FPGA高速安全状态机设计:编码、时序优化与安全机制

/* MD / 富文本中的 .toc(含博客园搬家等嵌套结构);.toc-box 在侧栏,不受影响 */#content_views .toc,/* 编辑器常在目录前后插入空 p(:empty 仍占 20px),一并去掉避免顶空隙 */#content_views.markdown_views > p:empty:has(+ .toc),#content_views.markdown_views …

2026/10/16 4:28:20 阅读更多 →
基于MP2642GR-Z与PIC18F87J50的两串电池主动均衡方案设计与实现

基于MP2642GR-Z与PIC18F87J50的两串电池主动均衡方案设计与实现

/* MD / 富文本中的 .toc(含博客园搬家等嵌套结构);.toc-box 在侧栏,不受影响 */#content_views .toc,/* 编辑器常在目录前后插入空 p(:empty 仍占 20px),一并去掉避免顶空隙 */#content_views.markdown_views > p:empty:has(+ .toc),#content_views.markdown_views …

2026/10/16 4:28:20 阅读更多 →
MNIST模型压缩实战:886个参数实现98.2%准确率

MNIST模型压缩实战:886个参数实现98.2%准确率

1. 项目缘起与核心思路拆解1.1 为什么要在 MNIST 上死磕参数量MNIST 手写数字识别大概是每个接触深度学习的人绕不开的第一个项目。LeNet-5 在 1998 年就能把它做到 99% 以上,现在随便搭一个三层卷积网络,几分钟训练就能摸到 99.2% 的准确率。也正因为如…

2026/10/16 4:28:20 阅读更多 →
深入理解Linux sync命令:从延迟写入到数据落盘

深入理解Linux sync命令:从延迟写入到数据落盘

1. 为什么需要sync:先看懂内核的延迟写入机制1.1 数据进入page cache之后发生了什么在Linux日常维护里,sync大概是看起来最不起眼的一个命令。它不像top、ps那样天天见,执行起来往往一秒不到就回到提示符,很多刚接触Linux的人甚至…

2026/10/16 4:28:20 阅读更多 →
前端导出Excel/CSV实战:从字符串拼接、Blob到大数据量性能优化

前端导出Excel/CSV实战:从字符串拼接、Blob到大数据量性能优化

1. 从一次数据交付事故说起:为什么前端导出不是“调个库”那么简单去年年底,我帮一个做运营后台的朋友处理过一个挺典型的烂摊子。他们的系统里有一个订单明细页,运营同事每天要把当天的数据导出来做二次分析。最初的做法是后端提供一个接口&…

2026/10/16 4:28:20 阅读更多 →
模板代码异常处理全攻略:从编译拦截到渲染降级

模板代码异常处理全攻略:从编译拦截到渲染降级

模板代码的异常处理,说难不难,说简单也不简单。我接过不少以模板渲染为核心的项目,从批量邮件通知、自动化报表到页面静态化,几乎每个项目在跑起来之后,第一波线上问题全出在模板这一层——变量名拼错、字段为空、循环…

2026/10/16 4:27:20 阅读更多 →

日新闻

个人 AI Agent 平台接入 TaoToken:统一 Key 与 API 通道的配置大纲

个人 AI Agent 平台接入 TaoToken:统一 Key 与 API 通道的配置大纲

/* MD / 富文本中的 .toc(含博客园搬家等嵌套结构);.toc-box 在侧栏,不受影响 */#content_views .toc,/* 编辑器常在目录前后插入空 p(:empty 仍占 20px),一并去掉避免顶空隙 */#content_views.markdown_views > p:empty:has(+ .toc),#content_views.markdown_views …

2026/10/16 0:00:05 阅读更多 →
信号与系统频域分析作业解题指南

信号与系统频域分析作业解题指南

1. 作业背景与核心要求解析信号与系统作为电子信息类专业的核心基础课,其作业设计往往直接关联课程的核心知识框架。2026年春季第六次作业的布置,通常对应着课程进展到频域分析的关键阶段。根据常规教学进度推测,本次作业可能涉及以下核心内容…

2026/10/16 0:00:05 阅读更多 →
Django MVT实战:从零构建图书借阅系统,彻底搞懂Model-View-Template分层

Django MVT实战:从零构建图书借阅系统,彻底搞懂Model-View-Template分层

1. 从一个"四不像"的练手项目说起如果你已经看完了Django的官方教程前几章,跟着敲完了投票应用,大概会有一种"我好像会了,又好像什么都不会"的割裂感。教程里每个知识点都是拆开的——这一章讲模型,下一章讲视…

2026/10/16 0:00:05 阅读更多 →

周新闻

流感时间序列预测实战:ARIMA/LSTM全流程拆解与避坑指南

流感时间序列预测实战:ARIMA/LSTM全流程拆解与避坑指南

简介:基于 ARIMA、LSTM、Transformer 等模型的流感时间序列预测 Python 源码,面向计算机相关专业课程设计与期末大作业学生,以及项目实战学习者。内容覆盖预处理、平稳性检验、定阶、残差分析、多模型对比预测的完整时序建模流程,…

2026/10/15 16:18:54 阅读更多 →
影刀RPA新手教程:键盘模拟输入实战——输入文本与模拟按键的区别

影刀RPA新手教程:键盘模拟输入实战——输入文本与模拟按键的区别

影刀RPA新手教程:键盘模拟输入实战——输入文本与模拟按键的区别 做影刀RPA自动化,十个新手有八个栽在"往输入框里填东西"这件事上:要么填不进去,要么填了一半,要么直接把原来内容追加在后面。这背后的根因&…

2026/10/15 19:41:51 阅读更多 →
影刀RPA新手教程:阅文起点小说数据采集实战——书籍信息与章节内容

影刀RPA新手教程:阅文起点小说数据采集实战——书籍信息与章节内容

影刀RPA新手教程:阅文起点小说数据采集实战——书籍信息与章节内容 1. 认识影刀:什么场景该用RPA采小说数据 起点中文网的页面结构相对稳定——分类榜单、书籍详情、章节内容三块独立页面,跳转链路清晰。这种场景非常适合影刀自动化&#x…

2026/10/15 13:48:15 阅读更多 →

月新闻

我发现了一个新思路:用 Remotion + Claude Code 像写代码一样自动化生成短视频

我发现了一个新思路:用 Remotion + Claude Code 像写代码一样自动化生成短视频

/* MD / 富文本中的 .toc(含博客园搬家等嵌套结构);.toc-box 在侧栏,不受影响 */#content_views .toc,/* 编辑器常在目录前后插入空 p(:empty 仍占 20px),一并去掉避免顶空隙 */#content_views.markdown_views > p:empty:has(+ .toc),#content_views.markdown_views …

2026/10/15 0:14:36 阅读更多 →
Windows下 Codex 中 Chrome 和 Computer Use 插件不可用问题排查及解决参考方式:TaoToken 统一 Key 配置与验证

Windows下 Codex 中 Chrome 和 Computer Use 插件不可用问题排查及解决参考方式:TaoToken 统一 Key 配置与验证

/* MD / 富文本中的 .toc(含博客园搬家等嵌套结构);.toc-box 在侧栏,不受影响 */#content_views .toc,/* 编辑器常在目录前后插入空 p(:empty 仍占 20px),一并去掉避免顶空隙 */#content_views.markdown_views > p:empty:has(+ .toc),#content_views.markdown_views …

2026/10/16 3:49:43 阅读更多 →
黑夜航拍船只数据集训练YOLOV5模型全流程解析

黑夜航拍船只数据集训练YOLOV5模型全流程解析

/* MD / 富文本中的 .toc(含博客园搬家等嵌套结构);.toc-box 在侧栏,不受影响 */#content_views .toc,/* 编辑器常在目录前后插入空 p(:empty 仍占 20px),一并去掉避免顶空隙 */#content_views.markdown_views > p:empty:has(+ .toc),#content_views.markdown_views …

2026/10/15 9:49:14 阅读更多 →