9.2 实战:API 网关与 IM 长连接服务


9.2 实战:API 网关与 IM 长连接服务

本节摘要:两个经典产品形态各给一套完整装配:网关 = 前端连接池 + 后端连接池 + 流量搬运工位;IM = 会话注册表 + 推送路由 + 心跳踢人。本节实现核心链路代码,并说明各设施如何复用前八章的部件。

一、形态一:API 网关——两个车间对怼

网关的本质是"接前端的连接、开到后端的连接、在中间搬运字节"。难点不在转发本身,而在两头的连接管理与背压贯通:

public class GatewayFrontHandler extends ChannelInboundHandlerAdapter { private volatile Channel backend; // 与后端配对的连接 @Override public void channelActive(ChannelHandlerContext ctx) { // 前端连接建立:同步打开到后端的连接(生产用连接池复用) backendPool.acquire().addListener((ChannelFutureListener) f -> { if (f.isSuccess()) { backend = f.channel(); backend.pipeline().addLast(new BackendRelay(ctx.channel())); } else { ctx.close(); // 后端拿不到 直接拒绝 } }); } @Override public void channelRead(ChannelHandlerContext ctx, Object msg) { if (backend != null && backend.isActive()) { backend.writeAndFlush(msg); // 前到后:直搬 } else { ReferenceCountUtil.release(msg); // 4.1 账本:搬不动也要销账 } } @Override public void channelInactive(ChannelHandlerContext ctx) { if (backend != null) backend.close(); // 一断俱断 防半开 backendPool.release(backend); } } // 后端回程:后端响应搬回前端 public class BackendRelay extends ChannelInboundHandlerAdapter { private final Channel front; public BackendRelay(Channel front) { this.front = front; } @Override public void channelRead(ChannelHandlerContext ctx, Object msg) { if (front.isActive()) front.writeAndFlush(msg); else ReferenceCountUtil.release(msg); } }

网关的完整装配图与各设施的出处:入口限流(8.2 的 ConnectionThrottle)、前端协议解码(5.2 LengthField 或 5.3 HTTP)、转发(上面两个 Handler)、背压贯通(7.3 的 autoRead 开关——后端变慢时反过来暂停前端读取,而不是把响应囤在网关内存里)、优雅摘流量(8.3 停机序列)。生产网关还要加路由表与熔断(8.3 的 SimpleBreaker 按后端分组部署),但骨架至此已经完整。

网关最经典的翻车:后端慢导致网关内存被响应囤爆。背压贯通是唯一解——autoRead 双向联动,让"慢"沿连接链路反向传导到客户端,而不是停在网关。

二、形态二:IM 长连接——路由表加推送

IM 的核心是"连接 → 用户"的双向映射与跨连接推送:

@ChannelHandler.Sharable public class ImRouterHandler extends SimpleChannelInboundHandler<ImPacket> { private static final ConcurrentHashMap<String, Channel> SESSIONS = new ConcurrentHashMap<>(); // userId → 连接 @Override protected void channelRead0(ChannelHandlerContext ctx, ImPacket pkt) { switch (pkt.type()) { case LOGIN -> { Channel old = SESSIONS.put(pkt.userId(), ctx.channel()); if (old != null && old != ctx.channel()) { old.writeAndFlush(ImPacket.kicked()); // 顶号踢人 old.close(); } // 断开自动清表(6.2 双参 remove 防误删) ctx.channel().closeFuture().addListener( f -> SESSIONS.remove(pkt.userId(), ctx.channel())); ctx.writeAndFlush(ImPacket.ok()); } case MSG -> deliver(pkt); // 单聊:查表直投 default -> ctx.writeAndFlush(ImPacket.bad()); } } private void deliver(ImPacket pkt) { Channel target = SESSIONS.get(pkt.to()); if (target == null || !target.isActive()) { offlineQueue.enqueue(pkt); // 离线入队 return; } target.writeAndFlush(pkt).addListener(f -> { // 7.1 写回执 if (!f.isSuccess()) offlineQueue.enqueue(pkt); }); } }

配套设施按前八章认领:心跳踢人(7.1 的 IdleStateHandler,服务端只配读空闲)、会话容量(8.2 全局上限)、离线队列(有界队列,防内存爆)、重连退避(8.3 状态机在客户端跑)、多机路由(单机注册表换 Redis 广播,收到"用户在本机"的广播才投递)。

IM 最经典的翻车:顶号踢人没处理旧连接清理,或者 closeFuture 清表用了单参 remove——新会话刚顶上号,旧连接的关闭回调把新映射误删,用户"莫名收不到消息"。6.2 的双参 remove 就是为这个场景准备的。

三、两个形态的装配对照

维度 API 网关 IM 长连接
连接形态 双向:前端进、后端出 单向:海量客户端进
核心数据结构 路由表 + 后端连接池 userId 与 Channel 映射表
背压关键点 后端慢要传导到前端 推送要尊重对端写水位
心跳用途 探活后端连接池 踢僵死客户端连接
容错重点 后端熔断分组 离线队列有界、顶号清理
复用章节 5.3、7.1、7.3、8.2、8.3 6.2、7.1、8.2、8.3

⚠️ 上线前自检五条:限流闸门生效(压测打出拒绝计数);背压联动有效(模拟慢后端,网关内存平稳);心跳踢人按时执行(造僵尸连接观察);优雅停机达标(发布时客户端重连成功率);离线队列有界(灌流量看是否触发丢弃告警)。五条全绿,才算出厂。

💡 关键直觉:网关是"搬运工车间",性能考题是搬运路径最短;IM 是"路由器车间",正确性考题是映射表与生命周期严格同步。产品形态千差万别,拆开都是前八章那几类工位的重新组合。

实战要点速记

  • 网关骨架:前后端配对连接、双 Relay 搬运、一断俱断。
  • 背压贯通:后端慢要反向暂停前端 autoRead,防止网关囤爆。
  • IM 骨架:userId 与 Channel 映射、顶号踢人、离线有界队列。
  • 双参 remove:映射表清理的防误删姿势,顶号场景必用。
  • 装配即复用:限流、心跳、熔断、停机全部来自前八章设施。
  • 出厂自检五条:闸门、背压、心跳、停机、离线队列,逐条压测验证。

作者与出处
原作者: 灏天文库
来源:灏天文库
整理: 灏天文库整理
由灏天文库平台收录,内容或由平台用户上传,仅供学习交流
发布者: 作者: 灏天文库 转发
评论区 (0)
U