首页 / 资讯中心 / 文章详情

仿QQ的H5聊天室源码:前后端分离IM与WebSocket实战

仿QQ的H5聊天室源码:前后端分离IM与WebSocket实战 ★ FEATURED ARTICLE
简介这是一份基于前后端分离的H5聊天室源码整体界面参照QQ聊天窗口支持多人群聊、交友与客服等场景适合有一定前端基础的开发者学习即时通讯系统的设计思路。项目定位为开源演示工程可用来快速搭建企业内部通讯、内网交流或社区交流平台实际业务功能可根据需求自行扩展。资源包共包含534个文件解压后约12.9MB主要文件类型有服务端脚本、前端框架组件、样式表、页面文档及各类图片分别承担后端接口、界面交互、页面布局和视觉资源等职责同时也提供数据库初始化文件与本地启动脚本便于快速跑通环境。当前已有412人学习或下载。通过阅读这份代码能梳理用户登录、好友列表、会话列表与聊天窗口等模块的实现思路掌握前后端配合、群聊消息流转和组件拆分方式为自研企业通讯或客服系统打下基础。1. 仿QQ的H5聊天室源码一套前后端分离的IM工程能解决谁的交付难题「带前后端H5聊天室源码、仿QQ聊天界面、多人群聊IM」这类标题通常指一套完整的前后端分离聊天室工程。它解决的痛点很直白不用从零写 WebSocket 连接管理、离线消息、会话列表和未读计数拿到手改改业务层就能当客服平台、兴趣社群或交友产品的前端H5端来用。适合三类人要自建客服系统的企业开发、做社区交友产品的初创团队、接外包私活想快速交付的个人开发者。不过先说句实在话这类源码真正的门槛根本不在仿QQ界面——界面是最不费劲的一层。翻车往往发生在部署上线那一刻连接为什么秒断、消息为什么重复、离线消息为什么压根收不到。这几件事标题里一个字都不会写。2. 前后端分离聊天室的架构拆解接入、路由、存储三层各管什么选型怎么定前后端分离项目实战里聊天室是最典型的一类——前端H5只负责展示和交互后端要扛住的是长连接、消息路由和可靠性。而交友平台和客服平台底层其实是同一套IM内核区别只在业务层一边是好友推荐和资料卡一边是工单分配和访客路由。理解这套架构比急着跑通代码更重要因为你后面所有的排错都要回到「这条消息到底在哪一层出了问题」这个原点。2.1 一套聊天室源码的三层职责为什么前端H5只是最外面一层接入层管连接WebSocket 的建立、销毁、心跳和鉴权。这一层决定了能承载多少在线用户、断线了能不能找回来。路由层管分发单聊、群聊、系统消息进来之后怎么找到目标用户的连接在线就推离线就落库存。存储层管数据MySQL 里是用户、好友关系、群成员、历史消息Redis 里是在线状态、未读数、会话摘要。很多人拿到源码先看页面这是本末倒置。仿QQ界面再像也只是接入层前面的皮。真正决定这套源码值不值得投入的是以下三件事连接表放在哪个进程里、鉴权是 token 还是裸传 userId、离线消息有没有落库。这三件事就是标题里「前后端」三个字的全部含义——前端是静态资源后端是 API 加 WebSocket中间用 token 关联换皮肤不动后端这才是「前后端分离」的交付价值。为什么不用 HTTP 轮询群聊广播场景下轮询是每个用户每隔几秒主动拉一次服务端没法主动推延迟至少三五秒HTTP 头和握手开销在几百人同时在线的群里就是灾难。WebSocket 一次握手建立双向长连接群聊广播时服务端直接遍历在线 Session 推送。这也是聊天室源码普遍选 WebSocket 而不是 SSE 的原因SSE 是单向的客服坐席和用户双向打字聊天时你得搭两条通道。2.2 连接管理是IM的心脏在线表、心跳与重连的取舍聊天室的高并发IM宣传真正的参考价值不在「并发」本身而在连接管理的取舍。连接表通常有两种形态。单机版用进程内 MapuserId 映射到 WebSocket Session推送就是查表取 Session 发消息。集群版用 Redis 做在线状态配合发布订阅或 MQ 做跨节点广播。判断一套源码的架构水位就看这条。心跳是另一个关键取舍。WebSocket 长连接挂在 Nginx 后面默认 idle 超时只有 60 秒左右所以客户端必须定时发 ping服务端回 pong双方以此确认连接还活着。常见间隔是 30 秒一次60 秒没收到 pong 就判定掉线主动清理连接表。断线重连则用指数退避1 秒、2 秒、4 秒、8 秒封顶 30 秒避免断网瞬间所有用户同时重连把服务打崩。可靠性的取舍也得心里有数。H5 聊天室源码一般做到「至少一次」投递配合客户端幂等去重而不是费大力气做「恰好一次」。也就是说消息可能重复但不会丢重复靠消息 ID 去重不丢靠离线落库。「至少一次 幂等」是这类源码里性价比最高的组合也是下文排错的主线。技术栈单机在线连接规模跨节点广播方式常见源码形态Java Spring Boot WebSocket数千到数万取决于内存和文件描述符自行接 Redis 发布订阅或 MQ多数前后端分离 IM 源码PHP Workerman单机数千需部署 GatewayWorker 集群老牌聊天室、客服系统源码Node.js Socket.IO单机数万Socket.IO 自带 Redis Adapter快速原型、前端为主的团队云厂商 IM SDK十几万以上厂商托管不愿维护底层、接受平台限制提示选型表里的规模和广播方式是判断源码能不能上生产的第一依据。号称「高并发」却只有进程内 Map 的单机跑没问题上两台就漏消息。2.3 源码拿到手先做三件事确认连接模型、鉴权方式和离线链路拿到一套聊天室源码先别跑前端先做代码体检。我用三组命令定位源码的架构边界。第一组看连接表实现在哪里第二组看有没有离线消息表第三组看握手鉴权怎么处理# 1. 确认连接表是内存 Map 还是 Redis / 消息队列 grep -rn static.*Map\|ConcurrentHashMap\|RedisTemplate\|ChannelGroup --include*.java src/ # 2. 确认离线消息是否落库MySQL 或 Redis 持久化 grep -rln offline_message\|offline_msg\|离线消息 --include*.sql --include*.java ./ # 3. 确认握手参数里有没有 token 鉴权 grep -rn token\|getRequestParameterMap\|auth --include*.java src/main/java/com/chat/endpoint/这三条命令的逻辑是如果连接表是static ConcurrentHashMap那它就是单机版部署时只能一台服务器扛全部连接想扩容就得改架构如果离线消息只有注释没有建表语句那离线用户的消息必然丢如果握手时只取参数不校验那任何人都能伪造 userId 冒充别人。源码拿到手先花半小时跑这三条命令省下的是一整周的线上排错时间。这套体检思路比什么「代码规范检查」都实际。3. 后端IM核心代码WebSocket鉴权、群聊路由、离线消息这样落地后端是整套源码的承重墙。我按最常见的 Spring Boot 原生 WebSocket 方案讲因为这版代码量最少、最好改也最容易让你理解消息在服务端到底怎么走。你拿到的 PHP 或 Node 版本回调名不一样但链路完全一样握手鉴权、消息分发、离线兜底。把这套逻辑吃透换语言只是换语法。3.1 连接建立与鉴权token 校验、旧连接顶替、握手失败码浏览器原生 WebSocket 不能自定义 Header所以 token 只能走 URL 查询参数。这是聊天室源码里最常见的鉴权通道也是很多人漏掉的安检口。连接建立的代码骨架如下ServerEndpoint(/chat) public class ChatEndpoint { // 单机版连接表userId - Session集群版要换成 Redis 发布订阅 private static final ConcurrentHashMapString, Session CLIENTS new ConcurrentHashMap(); OnOpen public void onOpen(Session session) { String token session.getRequestParameterMap() .getOrDefault(token, List.of()).get(0); String userId authToken(token); // 解析 JWT 或查 Redis失败返回 null if (userId null) { session.close(new CloseReason(CloseReason.CloseCodes.VIOLATED_POLICY, invalid token)); return; } session.getUserProperties().put(userId, userId); CLIENTS.put(userId, session); // 同一用户新连接顶掉旧连接 } }这段代码干了两件事第一鉴权失败直接关闭连接状态码 1008 表示策略违规而不是 1000 正常关闭这样客户端能区分「被拒绝」和「网络断开」第二同一 userId 的新连接覆盖旧连接避免用户换设备或刷新页面后出现双连接导致消息推给一个已经没在看的页面。生产环境里双连接会造成「消息显示已送达但用户没看到」的假象顶替逻辑是必须的。提示如果源码里握手时只取参数不校验或者 URL 里直接传 userId 就能连上这套源码的鉴权就是裸奔。改法是在握手时查一次 Redis 里的 token 有效期一次 Redis GET 的代价远低于上线后被刷消息的代价。3.2 消息路由单聊、群聊、系统消息的分发路径消息路由是服务端的核心循环。客户端发上来的每条消息都带着类型字段服务端根据类型决定发给谁。统一的消息协议字段大概是typesingle/group/system、toId、content、msgId、seq、timestamp。其中msgId是客户端生成的消息唯一 ID用来去重seq是服务端生成的全局递增序号用来排序。OnMessage public void onMessage(String raw, Session session) { Message msg objectMapper.readValue(raw, Message.class); String userId (String) session.getUserProperties().get(userId); msg.setFromId(userId); msg.setSeq(seqGenerator.next()); // 服务端统一编号保证全局有序 if (single.equals(msg.getType())) { sendToUser(msg.getToId(), msg); } else if (group.equals(msg.getType())) { ListString uids groupMemberService.uidList(msg.getToId()); for (String uid : uids) { sendToUser(uid, msg); // 在线直推离线落库 } } else if (system.equals(msg.getType())) { // 客服分配、进群退群通知等走和群聊相同的广播逻辑 } } private void sendToUser(String userId, Message msg) { Session target CLIENTS.get(userId); if (target ! null target.isOpen()) { target.getAsyncRemote().sendText(objectMapper.writeValueAsString(msg)); } else { offlineService.save(userId, msg); // 离线消息落库 } }这段逻辑的关键在sendToUser的「在线直推、离线落库」双分支。群聊分发时服务端循环群成员列表逐个判断在线状态这是最简单的实现也足够支撑几百人的活跃群。单聊同理只是目标只有一个。系统消息可以复用群聊的广播路径区别只是toId是群 ID 还是全部在线用户。这里有个性能边界要讲清楚如果有几万人在一个大群里循环几万次CLIENTS.get()会有明显 CPU 开销但 H5 聊天室源码的绝大多数场景是几十到几百人的群这个实现完全够用。真正要优化的是把群成员列表缓存到 Redis而不是每次消息都查一次 MySQL。3.3 离线消息与未读计数在线则推、离线则存、重连后按 seq 拉取离线消息是聊天室源码最容易偷工减料的地方。正确的链路分两步发送时目标离线就写offline_message表重连时客户端带上自己最后一条消息的lastSeq服务端把大于这个序号的消息一次性补齐。这张表的结构不需要花哨能查、能删、按用户走索引就够了CREATE TABLE offline_message ( id BIGINT AUTO_INCREMENT PRIMARY KEY, user_id VARCHAR(64) NOT NULL, msg_id VARCHAR(64) NOT NULL, seq BIGINT NOT NULL, payload TEXT NOT NULL, created_at DATETIME NOT NULL, KEY idx_user_seq (user_id, seq) ) ENGINEInnoDB DEFAULT CHARSETutf8mb4;未读计数则是另一套逻辑。会话列表里每个会话的红点数字不能靠查离线消息表 count因为消息可能已读但未离线。常见做法是 Redis 里维护一个会话维度的未读计数器客户端打开会话时上报已读服务端清零// 未读计数每次给用户推送单聊消息时对目标会话的 key 加一 redisTemplate.opsForValue().increment(im:unread: userId : conversationId); // 用户打开会话时读取并清零 ListString keys redisTemplate.keys(im:unread: userId :*); // 逐个累加得到总未读数返回给前端渲染会话列表红点参数选择上offline_message表建议保留 7 天数据定时任务清理过期记录Redis 未读 key 设置 7 天过期防止长期不登录的用户堆积大量无用 key。重连拉取的接口建议加上limit一次最多拉 200 条超过就分页避免用户离线一星期后重连瞬间拉爆带宽。3.4 幂等与落库消息 ID 去重、历史消息异步写消息落库是另一个性能杀手。如果每条聊天消息都同步 INSERT数据库会变成整个系统的瓶颈。常见做法是消息先走内存队列批量写库。同时客户端在网络抖动时经常重发同一条消息服务端必须做幂等用msgId作为去重键重复消息直接丢弃。// 幂等去重同一 msgId 24 小时内只处理一次 Boolean first redisTemplate.opsForValue() .setIfAbsent(im:dup: msg.getMsgId(), 1, 24, TimeUnit.HOURS); if (Boolean.FALSE.equals(first)) { return; // 重复消息直接丢弃 } // 异步批量落库先用队列缓冲攒够 50 条或 500ms 再批量 INSERT messageQueue.enqueue(msg); // 消费线程每 500ms 或队列满 50 条执行一次批量插入这个去重键的 TTL 设为 24 小时覆盖了客户端重试窗口又不会让 Redis 里堆积无用的 key。历史消息表建议按月份分表或者至少按created_at建索引因为你迟早要写「翻聊天记录」功能没有索引的聊天记录查询会拖垮整库。这一节说的两个点——幂等去重和异步批量写——是聊天室源码里「看起来能跑、跑起来会挂」的高发区务必检查现有代码有没有这两层。4. 前端H5仿QQ聊天界面会话列表、消息渲染、连接状态怎么处理前端H5这一侧难度不在 CSS 像不像 QQ而在于三件事WebSocket 连接的生命周期管理、消息到达后的顺序保证、以及聊天过程中不打断用户的操作流。很多人做直播互动或客服系统的 H5 端问题都出在连接状态没人管——页面切后台、网络切换、服务器重启任何一个场景都能让聊天静默失联。4.1 三段式布局与会话列表未读红点和最近会话从哪来仿QQ界面的核心布局是左侧会话列表、右侧聊天窗口、底部输入区。会话列表的数据不来自 WebSocket而是来自 HTTP 接口——进页面时拉一次最近会话和未读数之后靠 WebSocket 推送实时更新。模板骨架大概是template div classim-layout aside classconversation-list div v-forc in conversations :keyc.id clickopenConversation(c) img :srcc.avatar / div classconv-name{{ c.name }}/div i v-ifc.unreadCount 0 classunread-badge{{ c.unreadCount }}/i /div /aside section classchat-panel header{{ currentConversation.name }}/header div classmessage-list refmessageList div v-form in visibleMessages :keym.seq :class[message-row, m.fromMe ? mine : other] img :srcm.avatar classavatar / div classbubble{{ m.content }}/div /div /div div classinput-area textarea v-modeldraft keydown.enteronEnter/textarea button clicksend发送/button /div /section /div /template关键点在于未读红点的数据流进入页面拉 HTTP 接口拿到初始未读数WebSocket 推送新消息时如果当前不在该会话未读数加一如果就在当前会话未读数清零并上报已读。这个逻辑听起来简单但很多人把未读数存到前端本地结果换设备后红点全丢或者多端登录时红点不同步。未读数一定要走后端 Redis前端只负责展示。4.2 WebSocket客户端封装心跳、指数退避重连、待发队列连接管理必须封装成一个独立模块不能在业务组件里裸写new WebSocket()。这个封装要处理四件事握手带 token、心跳保活、断线重连、断线期间的消息暂存。核心代码function useChatSocket({ url, token, onMessage }) { let ws null let retry 0 let heartbeatTimer null const MAX_RETRY 10 const pendingQueue [] // 断线期间用户输入的消息暂存 function connect() { ws new WebSocket(${url}?token${token}) ws.onopen () { retry 0 startHeartbeat() flushPending() // 连接恢复后先补发暂存消息 } ws.onmessage (e) { const data JSON.parse(e.data) if (data.type pong) return // 心跳响应不做业务处理 onMessage(data) } ws.onclose () { stopHeartbeat() if (retry MAX_RETRY) { const delay Math.min(30000, 1000 * 2 ** retry) // 1s 2s 4s 8s...封顶 30s setTimeout(connect, delay) retry } } ws.onerror () ws.close() // 统一走 onclose 重连 } function sendMessage(msg) { if (ws ws.readyState WebSocket.OPEN) { ws.send(JSON.stringify(msg)) } else { pendingQueue.push(msg) // 连接没建立先排队 } } function flushPending() { while (pendingQueue.length) ws.send(JSON.stringify(pendingQueue.shift())) } return { connect, sendMessage } }指数退避的意义是防止断网恢复那一刻所有用户同时重连把服务端打崩。MAX_RETRY到 10 次后停止重连提示用户手动刷新而不是无限重试把电池和流量耗光。pendingQueue 和离线消息是两回事pendingQueue 是用户还没发出去的输入重连后补发离线消息是服务端没推送过来的时段消息靠lastSeq拉取。4.3 消息渲染与「正在输入」气泡对齐、seq排序、输入法误发消息渲染的核心是排序。WebSocket 本身保证单连接消息有序但重连后增量拉取的离线消息和实时消息会并发到达如果不按 seq 排序客户端会看到消息顺序错乱。合并排序函数function mergeMessages(existing, incoming) { const map new Map(existing.map(m [m.seq, m])) incoming.forEach(m map.set(m.seq, m)) return [...map.values()].sort((a, b) a.seq - b.seq) }按 seq 而不是按到达时间排序是因为时间戳精度只到毫秒同一毫秒内的两条消息可能颠倒服务端生成的 seq 严格递增不会有这个问题。「正在输入」状态则要去抖处理用户每按一次键就发一个 typing 事件但 2 秒内最多发一次对方显示状态条后 2 秒无新事件就消失。这个功能在客服系统里几乎必备在交友平台里也是拉近距离的细节。还有一个小坑输入框用keydown.enter发送时中文输入法选词的回车会误触发送。生产环境必须监听compositionstart和compositionend在拼音组词期间不响应回车发送。这个坑几乎每个聊天室项目都会踩一次血泪经验。4.4 历史消息分页与滚动定位向上翻页不打断阅读聊天记录不能一次全加载。常见做法是进入会话先拉最近 50 条向上滚动到顶部时再拉上一页。滚动定位的关键是记录「加载前的内容高度」加载后把滚动容器的高度跳到相同位置用户才不会被强制拉到最上面async function loadMore() { const el messageList.value const prevHeight el.scrollHeight const prevTop el.scrollTop const older await fetchHistory({ conversationId, beforeSeq: visibleMessages[0].seq }) visibleMessages mergeMessages(older, visibleMessages) // 新数据插到前面 // 数据渲染完成后把滚动位置对齐到「刚才看到的这条消息」 requestAnimationFrame(() { el.scrollTop el.scrollHeight - prevHeight prevTop }) }消息特别多的时候每个 session 上千条 DOM 会让 H5 明显卡顿。简单方案是固定每条消息高度只渲染可视区域前后各一倍的消息用占位符撑起总高度这套「虚拟滚动」在聊天场景里够用不需要引入完整表格虚拟化方案。滚动定位和虚拟滚动是聊天体验里最容易被忽视但最影响手感的两件事上生产前务必验证。5. 聊天室源码接进生产环境连接层与消息层的5个高频坑这套源码从本地跑通到生产上线中间隔着的不是功能而是一串环境相关的坑。我自己排过的错里九成都集中在连接层和消息层。按排查顺序走先看连接能不能稳定维持再看消息会不会重复、丢失、乱序最后才去看界面表现。以下五条每一条都是真实线上事故。5.1 连接层三个坑Nginx秒断、鉴权裸奔、跨域联调第一个坑本地连 WebSocket 正常走域名访问永远是连上一两秒就断。浏览器的 Network 面板里能看到 101 切换协议的响应但紧接着 readyState 变成 3CLOSED服务端日志里也没有任何异常。原因是 Nginx 默认没有转发 WebSocket 的升级头连接建立后立即被当成普通 HTTP 请求关闭。location /chat/ { proxy_pass http://im-backend:8080/; proxy_http_version 1.1; proxy_set_header Upgrade $http_upgrade; proxy_set_header Connection upgrade; proxy_read_timeout 3600s; proxy_send_timeout 3600s; }proxy_http_version 1.1必须显式声明因为 Nginx 默认的 1.0 不支持 Upgrade 头。proxy_read_timeout默认 60 秒一旦超过这个时间没有数据流动Nginx 会主动断开连接。聊天场景里用户可能沉默两三分钟才打字所以这个超时时间要拉到 1 小时以上配合客户端心跳才能让长连接稳定存活。第二个坑握手鉴权裸奔。现象是抓包发现 WebSocket 连接地址里直接带着 userId改个参数就能冒充任何人收发消息。原因就是源码在握手时只取参数没做 token 校验。解决方法是接入层统一校验 token把 userId 从 token 里解出来URL 里的参数只作为 token 载体不作为身份凭据。这个校验不能省聊天室的伪造消息比普通接口越权更严重——用户会直接看到冒充他发的消息。第三个坑本地前后端联调时跨域报错WebSocket 握手失败。现象是前端跑在 5173后端跑在 8080浏览器报跨域。原因很常见分开发服务器没配代理。WebSocket 本身不受同源策略限制但服务端握手时校验了 Origin 头前端的域名不在白名单里。解决方法是开发环境配代理把 HTTP 和 WebSocket 都通过 Vite 转发到后端// vite.config.js export default { server: { proxy: { /api: http://localhost:8080, /ws: { target: ws://localhost:8080, ws: true } } } }生产环境则用 Nginx 把/api和/chat/放在同一个域名下从根上消灭跨域。同时服务端保留 Origin 白名单校验这是防止其他网站偷偷连你的聊天室服务的一种手段但它替代不了 token 鉴权——Origin 可以伪造token 才是第一道关。5.2 消息层两个坑重发导致重复、离线链路丢消息与顺序错乱第四个坑消息重复。现象是群聊里偶尔出现同一条消息刷两次时好时坏。原因是客户端在发送超时后自动重发但服务端没有幂等去重两条msgId相同的消息都被处理了。解决方法是服务端用 Redis 对msgId做 SETNX 去重重复消息直接丢弃见 3.4 节的代码。注意这个坑是「客户端没做超时重发就不会出现」但移动端网络环境复杂超时重发必须有所以幂等去重也必须同时有。第五个坑离线消息丢失或者重连后消息顺序错乱。现象是用户离线期间的群聊消息重连后一条都看不到偶尔能看到但顺序是乱的。原因有两处叠加一是离线消息写入时机不对只有目标离线才写库但群聊广播时服务端可能已经用「在线状态过滤」把离线用户跳过了二是重连后增量拉取的离线消息和实时消息直接 append 到列表末尾没有按 seq 合并排序。解决方法是统一「在线直推、离线落库」双分支逻辑离线表只在用户确实不在线时写入前端拉取增量后必须用 4.3 节的mergeMessages按 seq 合并不能简单拼接。这两步缺一离线状态就是黑匣子。6. 从能跑到抗压压测方法、上线检查清单与一次集群翻车教训代码跑通之后上线前先压测再调参数不然你永远不知道这套源码的边界在哪。最简单的压测是用 Node 写个几十行的脚本模拟几百个并发用户加入群聊观察消息送达率、P50/P99 延迟和失败率// 压测骨架200 个并发连接每人发 50 条群聊消息 const clients [] for (let i 0; i 200; i) { const ws new WebSocket(ws://your-host/chat?tokentest- i) ws.onopen () { for (let j 0; j 50; j) { ws.send(JSON.stringify({ type: group, toId: 1001, content: hello })) } } clients.push(ws) }压测时盯住三个指标错误连接数、消息送达率、P99 延迟。如果连接数一涨就报Too many open files那是系统文件描述符不够ulimit -n调大到 65535如果 Redis 延迟升高检查连接池配置和未读 key 的数量。上线前按下面这张清单过一遍能避开绝大多数环境类事故检查项常见参数翻车点Nginx 协议头Upgrade、Connection、read_timeout 3600s不自带默认配置必断文件描述符ulimit -n 65535连接数一上去先报 open filesRedis 连接池按并发连接数放大未读计数和在线状态高延迟JVM 堆内存-Xmx2g 起步消息缓冲堆积导致 OOM最后说一个我自己的翻车教训我曾把这套单机版聊天室源码直接部署到两台服务器以为负载均衡会自动分摊结果用户消息开始随机延迟。查了很久才发现连接表是进程内的 Map用户连在服务器 A群聊广播落在服务器 BB 的连接表里找不到这个 Session就把在线用户当成离线走了离线消息。于是两台机器的日志各自都对用户体验却是「对方明明在线却收不到」。后来改成 Redis 发布订阅广播把连接表从「进程内查表」变成「跨节点广播」才算解决。这类源码工程跑单机没问题一旦上集群连接管理必须换架构模型。如果团队还没有 Redis 和 MQ 的条件宁可先单机撑住也不要盲目堆两台机器。希望帮到你。本文还有配套的精品资源点击获取
阅读完成 · 觉得有帮助?
咨询建站