面向有 IM 选型困惑的中后端工程师。XMPP 是被流量重、XML 臃肿骂了十几年的老协议但它在协议扩展性、联邦互通、群聊/状态/离线这些基建成熟度上仍然是自研 WebSocket 之外最省力的另一条路。本文以ejabberdErlang/OTP为主线走读源码讲清楚XMPP 协议层 仿微信业务层 分布式扩容改造三段怎么拼顺手把 Qunar、Startalk 两家工业级改造当作对照案例拆给你看。源码及演示im.jstxym.top一、为什么 2026 年还值得看 XMPP 做 IM 底座先泼冷水微信不是XMPP 架构。微信是私有的长连接 写扩散 自研信令XMPP 是 IETF 标准化的 XML 流协议RFC 6120/6121扩展靠 XEP 系列。两者基因不一样。但选 XMPP 做仿微信的起点理由有三个1. 协议扩展已经把 IM 通用功能铺好了单聊RFC 6121message/、 roster 好友XEP-0144 Roster Item Exchange、群聊 MUCXEP-0045、消息归档 MAMXEP-0313、发布订阅 PUB-SUBXEP-0060、离线存储、在线状态 Presence——这些微信好友/群/消息历史/朋友圈的对应物XMPP 生态里都有 XEP 编号不用从头设计。2. Erlang/OTP 的并发模型天生贴 IMejabberd 单节点轻松 50 万 长连接业界公开口径Qunar 当年选型报告里给的数字Erlang 的 Actor 轻量进程 per-connection GC比 JVM 的线程模型在海量 idle connection场景下舒服得多。3. 分布式集群开箱有但要改才能到微信规模ejabberd 原生靠MnesiaErlang 分布式 DB EPMD 节点发现做集群两三节点没问题但要到千台级、跨 AZ、防脑裂工业界Qunar、Startalk都是保留 XMPP 协议面 换掉 Mnesia 接 Kafka/RocketMQ这套打法——这正是本文后半段的重点。 一个常被忽略的点XMPP 的流量重 criticism 在移动端是真的但服务端之间用 protobuf 替换 XML 流Qunar 的做法或客户端→服务端走 WebSocket 压缩能压掉一大截。别被十年前的刻板印象劝退。二、开源 XMPP 服务端选型Java 系 vs Erlang 系项目语言特点适合谁ejabberdErlang/OTP并发最强、集群最成熟、Qunar/Startalk 都基于它改要做仿微信规模、能接受 ErlangOpenfireJava插件机制友好、二次开发门槛低想 Java 栈一把梭、规模中小TigaseJava 部分 Erlang 思路性能介于两者之间商业支持多企业采购向ProsodyLua轻量、配置简单内部 IM / 小型场景本文主线走ejabberd——因为它是国内几家XMPP 改仿微信的真实底座Qunar 内部 IM、Startalk 都是 ejabberd fork源码走读的价值最高。三、ejabberd 源码走读一条消息的生命周期ejabberd 的代码结构简化ejabberd/ ├── src/ │ ├── ejabberd_c2s.erl # 客户端连接进程鉴权、XML 流解析、状态机 │ ├── ejabberd_router.erl # 核心路由器From/To/Stanza → 下一跳 │ ├── ejabberd_local.erl # 本地 JID 路由本节点 │ ├── ejabberd_sm.erl # Session ManagerJID → PID 映射 │ ├── mod_muc.erl # XEP-0045 群聊 │ ├── mod_mam.erl # XEP-0313 消息归档 │ ├── mod_roster.erl # 好友关系 │ ├── mod_presence.erl # 在线状态 │ └── mod_pubsub.erl # XEP-0060 发布订阅朋友圈级场景可复用3.1 客户端接入ejabberd_c2s 状态机每个 TCP/WS 连接上来ejabberd 起一个ejabberd_c2s进程Erlang 轻量进程万级连接内存压力很小。它本质是个gen_fsm / gen_statemwait_for_stream → wait_for_auth → session_established ↑ ↑ ↓ XML Stream SASL/PLAIN Stanza 循环关键代码片段概念性还原%% ejabberd_c2s.erl 核心回调handle_event({xmlstreamelement,Stanza},session_established,State)-Route#route{fromFrom,toTo,stanzaStanza},ejabberd_router:route(From,To,Stanza),{next_state,session_established,State};客户端发来的每条message/iq/presence/走到session_established状态后统一喂给ejabberd_router:route/3——这是全系统的消息总线入口。3.2 路由层ejabberd_router → ejabberd_sm → 目标 c2sejabberd_router:route(From, To, Stanza)的逻辑本地还是远端查To的 domain如果是本节点托管的 vhost →ejabberd_local; 如果是别的 ejabberd 节点 → 走 Erlang distributionTCP 内网转过去本节点内ejabberd_sm维护一张JID → PID的映射背后是 Mnesiasession表找到目标ejabberd_c2s进程 PID!一条消息过去目标 c2s收到后gen_statem:cast写到 TCP/WS 套接字%% 简化版 router 逻辑route(From,To,Stanza)-caseis_local(To#jid.lserver)oftrue-ejabberd_local:route(From,To,Stanza);false-ejabberd_s2s:route(From,To,Stanza)% server-to-serverend.这就是 XMPP 集群看起来简单的秘密——Erlang distribution 让跨节点像本地 PID 发消息一样透明小集群3–5 节点确实爽。但也是后面要改的命门。3.3 群聊 MUCmod_muc 的进程隔离XEP-0045 MUC 在 ejabberd 里是每个 room 一个 Erlang 进程%% mod_muc.erl 创建房间create_room(Host,RoomName,Opts)-Procgen_mod:get_module_proc(Host,?PROCNAME),gen_server:call(Proc,{create_room,RoomName,Opts}).%% 房间进程内部成员 list 消息广播handle_call({route,From,Packet},_From,State)-NewStatedo_add_user(From,State),broadcast(Packet,State),%% 遍历 occupant list 逐进程转发{reply,ok,NewState}.房间进程隔离的好处一个房间炸比如刷屏攻击不影响别的房间节点宕机后 room 进程可以在别的节点拉起配合 Mnesia 持久化 room 配置。3.4 消息归档 MAMmod_mam 接 MySQL/PG微信的消息漫游对应 XMPP 的 MAMMessage Archive Management, XEP-0313。ejabberd 的mod_mam把每条message/落库单聊双方各自归档一份可配置群聊room 维度归档存储后端Mnesia小体量/ MySQL / PostgreSQL / ODBC查询客户端发iqquery xmlnsurn:xmpp:mam:2.../query/iq带with/start/end/before/after分页⚠️ 原生 mod_mam Mnesia 在千万级消息后扛不住工业改造一般是MAM 接 MySQL 分库 ES 做检索——Startalk 就是这条路。四、仿微信功能怎么叠在 XMPP 上XMPP 开箱有单聊、群、roster、presence但微信特有的那几样——朋友圈、红包、音视频、公众号——XMPP 标准里没有得自己叠。叠的思路两种4.1 XEP-0060 PubSub 改朋友圈朋友圈本质是多生产者→多消费者 时间线。PubSub 语义刚好贴每个用户一个urn:user:{uid}:timelinenode发朋友圈 →PUBLISH到自己的 node好友订阅你的 noderoster 双向确认后自动 subscribe时间线拉取 →Items分页ejabberd 的mod_pubsub直接能用但要改两处才能像朋友圈node 权限模型从开放改成仅 roster 双向好友可 subscribeitem payload 从简单 XML 改成momenttext/images/likes//moment自定义结构4.2 红包自定义 IQ 后端事务红包不是 IM 协议层的事是业务层事务。走 XMPP 的iq typesetredpacketpacketId/total/count/toGroup//redpacket/iq后端单独起 Java/Go 服务接这块客户端 → ejabberd(c2s) → IQ 路由到 redpacket-serviceHTTP/gRPC→ 1. MySQL 建红包主记录 拆包明细行锁 / CAS 2. 成功后 ejabberd 代发一条 message typechatredpacket-ref packetId...//message 进群 3. 抢红包同理iq → redpacket-service → 更新明细 → 群内广播抢夺结果ejabberd 本身不管钱只当信令管道——这是 Qunar 架构里的标准做法TCP 长连接管状态和多端同步HTTP/gRPC 管业务事务。4.3 音视频XMPP 只做呼叫信令媒体走 SRTP/WebRTCXMPP 的 JingleXEP-0166是呼叫信令标准但工业界包括微信实际是自建媒体通道A → ejabberd → iqjingle actionsession-initiate.../jingle/iq → B ↓SDP 交换完 A ←→ B直连或 TURN 中继SRTP 加密不走 XMPP Serverejabberd 的mod_jingle可以省掉信令自研但媒体服你还得自己布 Coturn 媒体录制。五、分布式扩容从 ejabberd 原生集群到微信规模改造这一节是标题的硬钩子。分两层讲原生能做到什么、工业改造怎么做。5.1 ejabberd 原生集群小中型够用节点 A ←— Erlang Distribution (EPMD 4369) —→ 节点 B │ │ ├─ Mnesia replicated table (schema/roster/session) ├─ MUC room 配置 Mnesia 持久化 └─ session 表复制 → JID→PID 跨节点可见前端 LBHAProxy / LVS做 TCP 长连接负载均衡客户端连任意节点router 层自动跨节点投递。原生能扛的量级单节点 50 万长连接级别Erlang 进程 ~300 万内没事3–5 节点集群日常百万在线没问题。5.2 原生方案的三个天花板瓶颈根因Mnesia 集群脑裂网络分区时两节点各自写 session/roster合并冲突难解Mnesia 大表性能session、roster、MAM 消息量上去后Erlang ETS/Mnesia 的读写延迟漂移跨 AZ 延迟Erlang distribution 对 RTT 敏感跨机房直连抖所以国内做到微信竞品规模的几家都不是裸 ejabberd 上生产。5.3 工业改造样本一Qunar IMQunar 当年选型 ejabberd 的理由很直白Erlang 调度/GC 贴 IM 实时性、ejabberd 集群容错单节点挂不影响整体。但他们改了两刀网络传输层XML 流 →protobuf 替换补 XMPP 流量冗余的短板数据层高频放 Redis重数据走 MQ 解耦不直接怼 DB5.4 工业改造样本二Startalk去 Mnesia 那版Startalk 的改造更激进参考价值更高“去掉了依赖 mnesia 集群的代码来支持更大的集群以及防止由于网络分区导致的集群状态不一致通过 ejabberd 和 kafka 相连接实现消息的路由和订阅发布”拆出来的模块长这样ejabberd (c2s/router 保留) │ ├── Kafka → 消息路由 / 跨节点投递 / 离线补偿 ├── Redis → JID→node 映射、在线状态 Bitmap ├── MySQL(PostgreSQL) → roster、MAM、群关系 └── im_http_serviceJava/Tomcat→ 好友搜索、历史同步、业务逻辑关键改造点session 表迁出 Mnesia → RedisJID → {node, pid, lastActive}存 Redis Hash跨节点路由时查 Redis 而非 Mnesia 广播消除脑裂风险消息投递改 Kafkac2s 收到消息 → 落 MAM(MySQL) → 发 Kafkatopicuser:{uid}各节点消费自己托管的 uid 往 c2s 写——这一步把Erlang distribution 跨节点投递换成了 Kafka 持久化投递节点崩了消息不丢MAM 接 MySQL 分库按uid % N或month分表ES 做全文检索搜聊天记录文件服务独立qfproxyTomcat接 MinIO / OSS消息体里只塞file mediaId.../这一套改完ejabberd 退化成一个**“超高性能 XMPP 接入 路由壳”**状态与存储全外置——这才是仿微信架构在工程上成立的方式。5.5 扩容公式改造后在线容量 ≈ 单 ejabberd 节点长连接上限 × 节点数 ≈ 50万 × NN 受 Redis/Kafka 分区数限制 消息吞吐 ≈ Kafka partition 数 × 单 partition 吞吐 Kafka 这里一般不卡卡的是 MAM 落库那一段HAProxy 前端做 TCP LB → ejabberd 节点无状态session 在 Redis→ 扩节点只需新起 ejabberd 实例 注册到 RedisKafka 消费组自动 Rebalance。六、从零搭建源码编译 → 集群 → 仿微信功能叠加6.1 编译 ejabberdErlang/OTP 26gitclone https://github.com/processone/ejabberd.gitcdejabberd ./autogen.sh./configure --enable-mysql --enable-redis\--enable-pgsql --enable-stun\--with-rebar./rebar3makemakeinstall关键--enable-*决定你后面能不能接 MySQL/Redis建议一次开全。6.2 单节点配置yaml简化hosts:-im.example.comlisten:-port:5222module:ejabberd_c2scertfile:cert.pemstarttls:true-port:5280module:ejabberd_httprequest_handlers:/ws:ejabberd_http_ws# WebSocket 接入Web/小程序端auth_method:[sql]# 走 MySQL 存账号sql_type:mysqlsql_server:127.0.0.1sql_database:ejabberdsql_username:ejabberdsql_password:xxxmodules:mod_muc:host:conference.HOSTmax_users:500mod_mam:db_type:sql# MAM 落 MySQLmod_roster:sqlmod_presence:{}6.3 两节点集群原生版先跑通再改# 节点 AERLANG_COOKIEwxclone123ejabberdctl--nodeejabberdn1 start# 节点 BERLANG_COOKIEwxclone123ejabberdctl--nodeejabberdn2 start ejabberdctl--nodeejabberdn2 join_cluster ejabberdn1# 验证ejabberdctl--nodeejabberdn1 mnesia info# 能看到 n2 的 replicas 即成功HAProxy 前端mode tcp、balance roundrobin指到 n1:5222、n2:5222客户端无感。6.4 叠加朋友圈mod_pubsub 改最简 demo在mod_pubsub的 node 配置里加一条自定义 item 格式!-- 发朋友圈 --iqtypesettopubsub.im.example.compubsubxmlnshttp://jabber.org/protocol/pubsubpublishnodetimeline:uid123itemmomentxmlnsurn:custom:momenttext今天撸了个 XMPP/textimageshttp://cdn/xxx.jpg/images/moment/item/publish/pubsub/iq客户端 timeline 拉取就是retrieve-items分页靠max_itemsafter。七、XMPP 这条路 vs 上一轮 SpringCloudNetty 的选型对照维度XMPP/ejabberdSpringCloudNetty WS协议IETF 标准XEP 丰富自研 / 私有协议并发单节点Erlang 50 万 长连接JVM Netty 8–15 万8C16G群聊/状态/离线mod_muc/mod_mam 开箱自己写分布式小集群 Mnesia 爽大集群要改RocketMQ Redis 自己拼流量XML 重移动端需 protobuf 替换二进制 / JSON 可控二次开发Erlang 门槛高Java 系人手多联邦互通XMPP federated 原生无选型建议团队 Erlang hold 住、要快速出群漫游好友全套 → XMPP团队 Java 主力、要深度定制协议、规模中等 → SpringCloudNetty要微信那种朋友圈/红包/音视频 → 两条路都得自己在 XMPP 业务层或 Netty 业务层另叠协议层都不包八、压测与坑点压测工具tsungErlang 写的 XMPP 压测器ejabberd 亲儿子、jmeter-xmpp-plugin、自写 Erlang client 也行。常见坑XML 解析 CPU 高ejabberd 的fast_xml已经是 NIFC 实现但还是比二进制重——Qunar 换 protobuf 就是为此Mnesia 大集群抖动超过 7–9 节点建议直接走 Startalk 那套去 Mnesia Kafka改造别硬撑WebSocket 子协议浏览器端Sec-WebSocket-Protocol: xmpp要带对否则 ejabberd_http_ws 拒连EPMD 端口暴露生产务必防火墙封 4369 Erlang distribution 端口段否则集群被扫很危险JID 格式陷阱userdomain/resource里 resource 是同账号多端的区分键手机/pc/web 三个 resourcerouter 投递时full JID优先、bare JID兜底——多端同步的坑 80% 栽在这 XMPP 做仿微信的真身是拿 ejabberd 当高并发接入路由壳把 session/消息/状态外置到 RedisMySQLKafka再在 XEP-0060 PubSub、自定义 IQ、Jingle 信令这三层往上叠朋友圈/红包/音视频。协议标准化省掉的是设计聊天、群、好友、离线那 60% 的重复劳动代价是 Erlang 栈和 XML 流量这两道坎——前者靠团队后者靠 Qunar 那种 protobuf 替换就能压下去。源码走读建议顺序ejabberd_c2s.erl→ejabberd_router.erl→ejabberd_sm.erl→mod_muc.erl→mod_mam.erl五个文件读完XMPP IM 的骨架上就清了。要再深就去看 Startalk 对ejabberd_c2s的 patch——他们把 Mnesia 读写全换成 Redis/Kafka 的那几行 diff是分布式扩容这一节真正的答案。