本节摘要:两个经典产品形态各给一套完整装配:网关 = 前端连接池 + 后端连接池 + 流量搬运工位;IM = 会话注册表 + 推送路由 + 心跳踢人。本节实现核心链路代码,并说明各设施如何复用前八章的部件。
网关的本质是"接前端的连接、开到后端的连接、在中间搬运字节"。难点不在转发本身,而在两头的连接管理与背压贯通:
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 的核心是"连接 → 用户"的双向映射与跨连接推送:
@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 是"路由器车间",正确性考题是映射表与生命周期严格同步。产品形态千差万别,拆开都是前八章那几类工位的重新组合。