Spring Boot聊天系统可靠性设计:WebSocket+STOMP消息不丢方案

简介:这是一套基于SpringBoot的毕业设计级在线聊天系统源码,面向计算机类专业学生(如计科、人工智能、通信工程等)开展课程设计、期末大作业及毕设开发,也适合Java后端与移动端初学者进阶实践。系统采用前后端分离架构,整合Netty实现实时通信,前端基于MUI+H5Plus适配手机端,含登录注册、通讯录、朋友圈、扫一扫等完整功能模块;后台涵盖分布式文件存储(FastDFS)、Nginx负载均衡及多模块微服务协同(huxin-netty、huxin-mybatis等)。资源共290个文件,含73个Java核心业务类、74个编译后class文件、27个XML配置与映射文件、20个HTML页面及配套CSS/JS资源,包体仅1.35MB,轻量易部署。已有148人学习下载,提供可直接运行的调试通过代码、清晰分层的模块目录结构及典型聊天场景下的Handler、Utils、FileUtils等关键实现类,便于理解高并发IM系统的设计逻辑与工程落地细节。

1. 为什么一个 Spring Boot 聊天系统,毕业答辩时总被问“消息不丢吗”“并发撑得住吗”“离线消息怎么存”

这不是一个只拼前端炫酷动画、后端搭个 Controller 就能交差的“玩具项目”。当你把“基于 Spring Boot 的在线聊天系统”写进毕设标题,评审老师盯的从来不是登录页有没有圆角阴影,而是:WebSocket 连接断了重连时,用户刚发的那条“在吗”会不会石沉大海?100 人同时刷屏,消息顺序会不会乱成一锅粥?凌晨三点用户下线,早上上线时收到的 27 条未读,是靠内存轮询还是数据库兜底?这些问题背后,是真实系统必须面对的可靠性、一致性与可扩展性三座大山。本篇不讲“Spring Boot 多简单”,而是带你从零手撕一个能跑通、能压测、能解释清楚每行关键代码为什么这么写的聊天系统骨架——它用 Spring Boot 作底座,但真正值钱的是对长连接生命周期、消息投递语义、存储分层策略的落地选择。适合正在写毕设、想避开“功能能点开,细节一问就懵”陷阱的开发者;也适合想快速验证一个轻量级实时通信方案是否可行的工程实践者。


2. 用 WebSocket + STOMP 搭建可靠长连接通道:为什么不用纯 WebSocket 原生 API

2.1 选型依据:STOMP 是什么,它比裸 WebSocket 解决了哪三个硬伤

纯 WebSocket 协议本身只定义了“双向字节流通道”,不规定消息格式、订阅机制、错误重试或会话管理。这意味着你得自己设计:

  • 消息体是 JSON 还是 Protobuf?字段叫from还是senderId?
  • 用户 A 订阅“群聊#123”,B 发消息时,服务端怎么知道该推给谁?靠 HashMap 缓存 Session?宕机就全丢。
  • 网络抖动导致连接断开,前端重连后,如何让服务端知道“这是同一个用户,别清空他的订阅关系”?

STOMP(Simple Text Oriented Messaging Protocol)是一个运行在 WebSocket 之上的应用层子协议,它把上述问题标准化了:

  • 定义了CONNECT/SUBSCRIBE/SEND/DISCONNECT等标准帧(frame),客户端和服务端只需按规范解析;
  • 支持 Topic(广播)和 Queue(点对点)两种消息模型,天然适配“群聊”和“私聊”场景;
  • 内置receipt机制,可要求服务端对关键操作(如 SEND)返回确认,为消息可靠性提供基础。

提示:Spring Boot 对 STOMP 的支持封装在spring-boot-starter-websocket中,它底层仍用 WebSocket 传输,但帮你屏蔽了帧解析、心跳维持、Session 绑定等黑匣子逻辑。这不是“多一层抽象就慢”,而是用标准换来的可维护性。

2.2 最小可运行配置:5 行 Java 代码开启 STOMP 端点

@Configuration @EnableWebSocketMessageBroker public class WebSocketConfig implements WebSocketMessageBrokerConfigurer { @Override public void configureMessageBroker(MessageBrokerRegistry config) { // 1. 启用 /topic 前缀的广播消息(群聊、系统通知) config.enableSimpleBroker("/topic"); // 2. 设置 /user 前缀用于点对点消息(私聊),需配合 Security 配置 config.setUserDestinationPrefix("/user"); // 3. 客户端发送消息的目标地址前缀(如 /app/sendMsg) config.setApplicationDestinationPrefixes("/app"); } @Override public void registerStompEndpoints(StompEndpointRegistry registry) { // 4. 暴露 WebSocket 端点路径,允许跨域(毕设本地调试必备) registry.addEndpoint("/ws").setAllowedOrigins("*").withSockJS(); // 5. SockJS 是降级方案:当浏览器不支持 WebSocket 时,自动切到 HTTP 长轮询 } }

参数说明与踩坑点:

  • enableSimpleBroker("/topic"):启用内存级消息代理,适合单机开发。注意:它不持久化、不集群同步——如果项目后期要水平扩展,这里必须换成 Redis 或 RabbitMQ 代理(见第 5 章);
  • setUserDestinationPrefix("/user"):这是实现“用户 A 发消息给用户 B”的关键。Spring 会自动将/user/1001/private这类地址解析为“发给 userId=1001 的私有队列”,前提是用户登录后已通过SimpMessagingTemplate关联了 Session;
  • withSockJS():毕设阶段强烈建议保留。很多学校机房禁 WebSocket,但允许 HTTP,SockJS 能无缝 fallback,避免调试时“页面白屏却找不到原因”。

2.3 前端连接与订阅:用 stompjs 库 3 步完成握手

// 1. 创建 Stomp 客户端(使用 SockJS 作为传输层) const socket = new SockJS('/ws'); const stompClient = Stomp.over(socket); // 2. 连接并处理认证(假设后端用 JWT,token 存 localStorage) stompClient.connect( { Authorization: 'Bearer ' + localStorage.getItem('token') }, () => { console.log('WebSocket 连接成功'); // 3. 订阅两个关键频道:群聊(/topic/group/1)和私聊(/user/queue/private) stompClient.subscribe('/topic/group/1', onGroupMessage); stompClient.subscribe('/user/queue/private', onPrivateMessage); }, (error) => console.error('连接失败:', error) ); function onGroupMessage(message) { const data = JSON.parse(message.body); // 渲染群聊消息,注意 data.senderId, data.content 字段由后端统一约定 } function onPrivateMessage(message) { const data = JSON.parse(message.body); // 渲染私聊消息,data.fromId 即发送方 ID }

逻辑说明:
前端订阅/user/queue/private是一种“逻辑队列”,实际消息由后端根据当前用户 ID 动态路由。Spring 会自动将SimpMessagingTemplate.convertAndSendToUser("1001", "/queue/private", msg)发送的消息,精准投递给 userId=1001 的所有在线连接。这比前端自己维护“我该监听哪个 URL”更安全、更解耦。


3. 消息投递的三种语义:如何让“已发送”真正等于“对方已收到”

3.1 “最多一次”(At-Most-Once):默认行为,也是毕设最容易翻车的坑

Spring STOMP 默认采用“fire-and-forget”模式:服务端收到SEND帧,转发给订阅者,然后立即返回RECEIPT帧(如果客户端要求了)。但这个 RECEIPT 只代表“服务端已入队”,不代表“客户端已渲染”。如果用户网络瞬间中断,消息就丢了,且无任何补偿。

血泪经验:某高校毕设演示时,导师故意拔网线,学生发现“最后一条消息消失”,当场无法解释。根源就是没区分“服务端投递成功”和“客户端接收成功”。

3.2 实现“至少一次”(At-Least-Once):客户端 ACK + 服务端重发队列

核心思路:客户端收到消息后,主动发送一个ACK帧给服务端,服务端收到才认为投递成功;若超时未收到 ACK,则从重发队列中再次推送。

服务端关键代码(消息发送与 ACK 监听):

@Component public class ChatMessageService { @Autowired private SimpMessagingTemplate messagingTemplate; @Autowired private RedisTemplate<String, Object> redisTemplate; // 用 Redis 存重发队列 // 发送消息时,生成唯一 messageId,并存入 Redis(有效期 2 分钟) public void sendMessageToUser(String userId, ChatMessage message) { String messageId = UUID.randomUUID().toString(); message.setMessageId(messageId); // 先存重发队列:key=userId:retry, value={messageId, message, timestamp} String retryKey = "user:" + userId + ":retry"; redisTemplate.opsForHash().put(retryKey, messageId, message); redisTemplate.expire(retryKey, 2, TimeUnit.MINUTES); // 再通过 STOMP 发送 messagingTemplate.convertAndSendToUser(userId, "/queue/private", message); } // 监听客户端发来的 ACK(需前端在 onMessage 后调用 stompClient.ack(frame)) @MessageMapping("/ack") public void handleAck(@Payload AckMessage ack) { String retryKey = "user:" + ack.getUserId() + ":retry"; // 从 Redis 删除对应 messageId,表示投递成功 redisTemplate.opsForHash().delete(retryKey, ack.getMessageId()); } }

前端 ACK 触发逻辑:

function onPrivateMessage(message) { const data = JSON.parse(message.body); renderMessage(data); // 渲染消息 // 关键:渲染完成后,立即发送 ACK 帧 stompClient.ack(message); // stompjs 自动构造 ACK 帧 }

参数说明:

  • Redis TTL=2分钟:足够覆盖绝大多数网络抖动,又避免重发队列无限膨胀;
  • ACK 帧必须在渲染后触发:确保用户真正“看到”消息才确认,而非仅仅收到字节流;
  • 此方案成本低、改造小,毕设答辩时能清晰画出“发送→接收→ACK→删除”流程图,比空谈“消息可靠性”有力得多。

3.3 “恰好一次”(Exactly-Once):毕设不强求,但要知道它的代价

理论上可通过分布式事务(如 Seata)+ 消息去重表(message_id 唯一索引)实现。但代价是:

  • 每条消息增加 2 次数据库写入(事务日志 + 去重表);
  • 引入强依赖外部中间件,毕设环境部署复杂度陡增;
  • 对于 90% 的毕设场景,“至少一次”已满足需求(用户最多看到重复消息,但不会丢失)。

提示:答辩时若被问及,可坦诚回答:“我们采用 At-Least-Once,配合前端去重(如检查 message.id 是否已存在 DOM),在可用性与复杂度间做了权衡。若需 Exactly-Once,需引入 RocketMQ 事务消息或 Kafka 幂等 Producer,这超出当前项目范围。”


4. 消息存储分层设计:内存缓存 + MySQL 持久化 + Redis 热数据加速

4.1 为什么不能只用 MySQL?—— 毕设常见性能误判

很多同学一上来就@Entity+JPA,所有消息直写数据库。结果测试发现:

  • 100 人在线时,群聊消息每秒写入 50+ 条,MySQLINSERT成瓶颈;
  • 查询“最近 20 条历史”需要ORDER BY create_time DESC LIMIT 20,随着数据量增长,create_time索引失效,查询变慢;
  • 更致命的是:消息是典型的“写多读少”场景,但 MySQL 的 B+ 树索引对高频写入并不友好。

正确做法是分层:热数据放内存/Redis,冷数据落盘,查询走缓存穿透保护。

4.2 三层存储结构与数据流向

层级技术选型存储内容生命周期读写频率
L1:内存缓存ConcurrentHashMap当前在线用户的 Session 映射、未 ACK 消息队列JVM 生命周期高频读写(连接管理)
L2:RedisSorted Set(ZSET)每个群聊/私聊的最新 500 条消息(score=timestamp)7 天自动过期高频读,中频写
L3:MySQLInnoDB表全量消息归档(含 sender_id, receiver_id, content, status)永久低频写,中频查(后台管理)

关键设计点:

  • Redis ZSET 的score用毫秒时间戳,member存 JSON 序列化的消息对象,天然支持按时间倒序取 TOP N;
  • MySQL 表必须建联合索引:INDEX idx_user_time (receiver_id, create_time),支撑“查某用户所有消息并分页”;
  • 绝不允许 Redis 和 MySQL 数据不一致!所有写操作必须先写 MySQL(主库),再更新 Redis(Cache-Aside 模式)。

4.3 消息写入的原子化封装:一个方法搞定三写

@Service @Transactional // 确保 MySQL 写入成功才继续 public class MessagePersistenceService { @Autowired private JdbcTemplate jdbcTemplate; @Autowired private RedisTemplate<String, Object> redisTemplate; public void saveAndCacheMessage(ChatMessage message) { // 1. 写 MySQL(主库) String sql = "INSERT INTO chat_message (sender_id, receiver_id, content, create_time) VALUES (?, ?, ?, ?)"; jdbcTemplate.update(sql, message.getSenderId(), message.getReceiverId(), message.getContent(), message.getCreateTime() ); // 2. 写 Redis ZSET:群聊消息存入 /group/{id},私聊存入 /private/{userId} String redisKey = message.isGroupMessage() ? "group:" + message.getGroupId() : "private:" + message.getReceiverId(); // ZADD key score member,score 为毫秒时间戳 redisTemplate.opsForZSet().add(redisKey, message.toJsonString(), message.getCreateTime().getTime() ); redisTemplate.expire(redisKey, 7, TimeUnit.DAYS); // 7天过期 // 3. (可选)异步清理 L1 内存中的临时队列(如未 ACK 列表) if (message.isUnack()) { asyncClearUnackQueue(message.getMessageId()); } } }

参数说明:

  • @Transactional是底线:MySQL 写失败,整个方法回滚,Redis 不会脏写;
  • redisTemplate.expire(...)必须显式设置,否则 ZSET 永不过期,Redis 内存爆炸;
  • 私聊消息的 Redis Key 设计为private:{userId},意味着每个用户有自己的消息池,查询时直接ZREVRANGE private:1001 0 19即可拿到最新 20 条,O(log N) 复杂度。

5. 避坑指南:毕设调试中最常遇到的 4 个“玄学”问题与根治方案

5.1 现象:前端连接 WebSocket 成功,但stompClient.subscribe()后收不到任何消息

原因:

  • 后端configureMessageBroker()中未调用config.setApplicationDestinationPrefixes("/app"),导致客户端stompClient.send("/app/sendMsg", ...)发送的消息,服务端根本没注册对应的@MessageMapping方法;
  • 或@MessageMapping方法参数未加@Payload注解,Spring 无法反序列化 JSON;
  • 更隐蔽的是:@EnableWebSocketMessageBroker注解缺失,整个 STOMP 配置类被 Spring 忽略。

解决:

  • 检查WebSocketConfig类是否被@Configuration修饰,且@EnableWebSocketMessageBroker存在;
  • 在@MessageMapping方法上加@ResponseStatus(HttpStatus.OK),用 Postman 模拟发送POST /app/sendMsg,看是否 200;
  • 开启 Spring DEBUG 日志:logging.level.org.springframework.web.socket=DEBUG,观察DEBUG o.s.w.s.m.StompSubProtocolHandler - Processing SUBSCRIBE frame是否出现。

5.2 现象:用户 A 给用户 B 发私聊,B 收到消息,但消息体里senderId是 null

原因:

  • 前端发送SEND帧时,JSON body 中senderId字段名与后端ChatMessage实体类的@JsonProperty注解不一致(如前端传"fromId",后端期望"senderId");
  • 或ChatMessage类缺少无参构造函数,Jackson 反序列化失败,静默创建空对象。

解决:

  • 统一前后端字段命名,推荐全部小驼峰(senderId,receiverId);
  • ChatMessage类必须有public ChatMessage() {};
  • 在@MessageMapping方法参数前加@Valid,配合@NotNull注解,让校验失败时抛出MethodArgumentNotValidException,便于定位。

5.3 现象:本地调试一切正常,部署到学校服务器后 WebSocket 连接 404

原因:

  • 学校服务器 Nginx/Apache 未配置 WebSocket 代理,HTTP 请求被正常转发,但 WebSocket 升级请求(Upgrade: websocket)被拦截或转成普通 HTTP;
  • 或服务器防火墙关闭了 WebSocket 默认端口(80/443 之外的端口)。

解决:

  • Nginx 配置必须包含:
    location /ws { proxy_pass http://backend; proxy_http_version 1.1; proxy_set_header Upgrade $http_upgrade; # 关键!透传 Upgrade 头 proxy_set_header Connection "upgrade"; # 关键!透传 Connection 头 proxy_set_header Host $host; }
  • 毕设部署优先走 80/443 端口,避免额外开防火墙;
  • 用curl -i -N -H "Connection: Upgrade" -H "Upgrade: websocket" http://your-server/ws测试升级头是否透传。

5.4 现象:MySQL 消息表数据越来越多,SELECT * FROM chat_message WHERE receiver_id = ? ORDER BY create_time DESC LIMIT 20查询变慢

原因:

  • receiver_id字段未建索引,全表扫描;
  • 即使有索引,当receiver_id区分度低(如大量消息发给系统用户 0),MySQL 优化器可能放弃索引,改用 filesort。

解决:

  • 必建联合索引:ALTER TABLE chat_message ADD INDEX idx_receiver_time (receiver_id, create_time);
  • 查询时强制使用索引(仅调试用):SELECT * FROM chat_message FORCE INDEX (idx_receiver_time) WHERE receiver_id = ? ORDER BY create_time DESC LIMIT 20;
  • 生产环境考虑分区:按create_time月分区,避免单表过大。

6. 毕设答辩加分技巧:用 JMeter 做 3 分钟压测,证明你的系统不是“纸糊的”

6.1 为什么压测是答辩“王炸”?—— 导师最怕学生说不清边界

很多同学答辩时被问“能支持多少人”,回答“应该可以吧”“没试过”。而如果你能打开 JMeter,展示一张“100 并发用户下,WebSocket 连接成功率 100%,消息平均延迟 < 200ms”的截图,导师立刻明白:你不仅写了代码,还思考了它的工程边界。

6.2 三步搭建最小压测脚本(无需写代码)

Step 1:录制 WebSocket 流量

  • 启动 JMeter → 添加线程组 → 添加WebSocket Open ConnectionSampler;
  • Server Name/IP 填localhost,Port 填8080,Path 填/ws;
  • 添加WebSocket Single Write Sampler,Payload Type 选Text,内容填{"type":"CONNECT","headers":{}};
  • 运行,用浏览器访问聊天页面,JMeter 自动捕获SUBSCRIBE和SEND帧。

Step 2:参数化用户与消息

  • 在线程组下添加CSV Data Set Config:
    • Filename:users.csv(内容:1001,1002,1003,...)
    • Variable Names:userId
  • 将WebSocket Single Write Sampler的 Payload 改为:
    {"type":"SEND","destination":"/app/sendMsg","body":"{\"senderId\":\"${userId}\",\"receiverId\":\"1002\",\"content\":\"Hello from ${userId}\"}"}

Step 3:执行并分析结果

  • 线程数设为100,Ramp-Up Period10秒(模拟 10 秒内 100 人接入);
  • 添加View Results Tree和Summary Report;
  • 运行后重点关注:
    • Connect Time:应稳定在 50ms 内(证明连接池配置合理);
    • Latency:WebSocket 消息端到端延迟,< 300ms 为优秀;
    • Error %:必须为 0(证明无连接泄漏、内存溢出)。

我的习惯:毕设答辩前夜,用 JMeter 跑 3 轮不同并发(50/100/200),截图保存Summary Report,答辩时直接投屏。当导师问“稳定性如何”,我不说“理论上”,而是说:“这是 100 并发下的实测数据,错误率 0,平均延迟 186ms。如果需要更高并发,我会将 Redis 代理升级为集群,并增加 WebSocket 连接数限制。” —— 这种回答,比背十页原理管用。希望帮到你。

本文还有配套的精品资源,点击获取

相关新闻

用Python卷积神经网络实现几何形状识别:毕设实战全解析

用Python卷积神经网络实现几何形状识别:毕设实战全解析

如果把毕业论文当成一道需要反复打磨的作品&#xff0c;用Python做卷积神经网络&#xff08;CNN&#xff09;去识别几何形状&#xff0c;是我目前见过性价比最高、也最适合把深度学习理论落到代码的毕设方向之一。图形分类不像人脸识别、语义分割那么复杂&#xff0c;但麻雀虽小…

2026/10/20 19:50:46 阅读更多 →
日志系统的双刃剑:如何设计一个生产环境下可动态开启且不影响实时性的 printf 重定向方案

日志系统的双刃剑:如何设计一个生产环境下可动态开启且不影响实时性的 printf 重定向方案

摘要&#xff1a; 日志是嵌入式调试的刚需&#xff0c;但日志系统设计不当会直接破坏实时性——这一点在第 2 篇《你的 printf 正在拖垮实时性》里已经用真实事故验证过。然而那篇只给出了"环形缓冲区 DMA"的骨架&#xff0c;生产环境的日志系统还需要解决更多问题&…

2026/10/20 19:50:46 阅读更多 →
C++基础入门教程(七):一些比较特别的基础语法总结

C++基础入门教程(七):一些比较特别的基础语法总结

前言 入门教程写到第七篇&#xff0c;语法点看起来都学过了&#xff0c;但真正写代码时出错的地方&#xff0c;往往不是类和多态&#xff0c;而是那些"以为自己懂了"的基础规则&#xff1a;逗号表达式到底返回什么、Widget w(); 为什么不是定义对象、-1 < 1u 为什…

2026/10/20 19:50:46 阅读更多 →

最新新闻

YOLO26目标检测训练到RK3588部署:ONNX与RKNN适配实践

YOLO26目标检测训练到RK3588部署:ONNX与RKNN适配实践

YOLO26 目标检测的训练到瑞芯微平台适配&#xff0c;这条链路我最近刚完整跑通一遍。先说结论&#xff1a;真正卡人的地方不在训练本身&#xff0c;而在从 PyTorch 模型走向 RKNN 运行时的那几步——ONNX 导出、算子兼容、INT8 量化&#xff0c;每一个都埋着不少雷。我是在给一…

2026/10/20 20:43:28 阅读更多 →
梯度增强PINN:攻克Allen-Cahn方程多陡峭界面难题

梯度增强PINN:攻克Allen-Cahn方程多陡峭界面难题

Allen-Cahn方程是我调过的最“拧巴”的PDE之一。它表达式极简&#xff0c;一个∂u/∂t ε∂u/∂x u - u&#xff0c;但解的画面完全不简单&#xff1a;能量驱动下的相分离会让解在空间里劈出好几条尖锐的界面&#xff0c;这些界面随时间演化、移动、合并。所谓“多非常陡峭区…

2026/10/20 20:43:28 阅读更多 →
计算机教材内容策划与写作:从概念到工程实践

计算机教材内容策划与写作:从概念到工程实践

抱歉&#xff0c;我无法处理这个请求。

2026/10/20 20:43:28 阅读更多 →
从图像到数据:HaoCurve曲线提取实战与避坑指南

从图像到数据:HaoCurve曲线提取实战与避坑指南

简介&#xff1a;HaoCurve&#xff08;薅曲线&#xff09;是一套面向科研人员、工程师与数据分析者的MATLAB曲线绘制工具源码&#xff0c;适合需要在MATLAB环境中快速生成、编辑和分析数据曲线图的用户&#xff0c;尤其对希望深入理解绘图脚本实现、进行二次开发的进阶使用者更…

2026/10/20 20:43:28 阅读更多 →
CLI编码代理失败过程解剖:从轨迹日志到根因分析

CLI编码代理失败过程解剖:从轨迹日志到根因分析

读到一篇很有意思的论文&#xff0c;标题叫《Failure as a Process: An Anatomy of CLI Coding Agent Trajectories》。这两年命令行AI编程工具越来越多&#xff0c;从最初的“能写一段函数”到现在能在真实仓库里自己定位问题、改代码、跑测试&#xff0c;甚至提交补丁。但大多…

2026/10/20 20:43:28 阅读更多 →
小橘子V4.790:本地可调试塔防策略模拟器

小橘子V4.790:本地可调试塔防策略模拟器

简介&#xff1a;本资源是《向僵尸开炮》游戏的自动化对局辅助工具——小橘子V4.790版本完整部署包&#xff0c;面向游戏AI开发者、自动化测试工程师及休闲游戏技术爱好者&#xff0c;用于快速实现人机对战环境下的策略执行、界面识别与操作模拟。压缩包共741个文件&#xff0c…

2026/10/20 20:42:25 阅读更多 →

日新闻

CLRC663芯片Pin-to-Pin替代实战:从评估到量产全记录

CLRC663芯片Pin-to-Pin替代实战:从评估到量产全记录

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

2026/10/20 1:01:30 阅读更多 →
ESP32舵机控制指南:原理、多路驱动与避坑实战

ESP32舵机控制指南:原理、多路驱动与避坑实战

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

2026/10/20 1:01:30 阅读更多 →
位移法实现一阶低通滤波:单片机ADC采样降噪实战

位移法实现一阶低通滤波:单片机ADC采样降噪实战

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

2026/10/20 1:01:30 阅读更多 →

周新闻

基于GMM的语音识别系统原理与MATLAB实现详解

基于GMM的语音识别系统原理与MATLAB实现详解

说到基于高斯混合模型&#xff08;GMM&#xff09;的语音识别系统&#xff0c;很多人第一反应就是一堆数学公式&#xff0c;觉得门槛很高。但如果你真的在MATLAB里把它落地跑通&#xff0c;会发现核心其实就是三件事&#xff1a;把声音变成特征向量&#xff0c;用GMM给每个类别…

2026/10/20 10:02:13 阅读更多 →
数字图像处理入门:反白、灰度化、调色板修改与导出txt实践

数字图像处理入门:反白、灰度化、调色板修改与导出txt实践

数字图像处理这四个字听起来挺唬人&#xff0c;但落到具体操作上&#xff0c;无非就是让计算机按你的想法“摆弄”像素。我在一个模拟项目X里用了一套特别经典的基础处理流程&#xff1a;对图像做反白、改调色板、彩色转灰度&#xff0c;最后把像素数据导出成txt文件。单看每一…

2026/10/19 17:32:41 阅读更多 →
机器人大脑技术盘点:具身智能的感知、决策与数据闭环

机器人大脑技术盘点:具身智能的感知、决策与数据闭环

如果你的信息流最近也天天刷到“机器人大脑”这个词&#xff0c;你会发现一个很有意思的现象&#xff1a;每个人说的“大脑”根本不是同一个东西。有人指的是大语言模型&#xff0c;有人指的是运动控制系统&#xff0c;还有人指的是能理解物理世界的端到端模型。在2026年这个时…

2026/10/20 2:50:50 阅读更多 →

月新闻

我发现了一个新思路:用 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/20 10:02:08 阅读更多 →
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/20 9:50:17 阅读更多 →
黑夜航拍船只数据集训练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/20 9:50:45 阅读更多 →