开启keepAlive的ApolloServer如何感知客户端订阅连接不可达并触发通知
解决Apollo Server订阅中客户端异常断开的检测问题
我来帮你搞定这个问题——你遇到的情况其实是WebSocket异常断开时的常见痛点:当客户端突然断网、进程崩溃这类异常断开时,TCP连接不会立刻触发服务器端的断开事件,导致onDisconnect钩子没法执行,自然没法同步数据库里的设备连接状态。下面是具体的解决思路和实现代码:
原理说明
你已经配置了keepAlive: 10000,这个参数会让服务器每隔10秒给客户端发一次WebSocket Ping帧。如果客户端正常在线,会回复Pong帧;要是客户端异常断开,服务器就收不到Pong响应,咱们就可以利用这个机制来检测客户端是否可达。
具体实现步骤
我们要在客户端连接时,给每个WebSocket绑定Ping/Pong的监听逻辑,跟踪客户端的存活状态:
- 在
onConnect里设置存活超时定时器:客户端连接成功后,设置一个超时时间(比keepAlive稍长),如果在这个时间内没收到Pong,就判定客户端已断开,执行数据库更新。 - 监听
pong事件重置定时器:每次收到客户端的Pong回复,就重置定时器,证明客户端还在线。 onDisconnect中清理定时器:正常断开时把定时器清掉,避免内存泄漏。
修改后的代码示例
const server = new ApolloServer({ typeDefs: typeDefs, schemaDirectives: { role: RoleDirective }, resolvers, context: async ({req, connection}) => { if (connection) { console.log('CONNECT with connection') return connection.context } else if (req) { console.log('CONNECT with request') let token = req.headers.authorization || ''; return await createContextFromRequest(req, token) } else { console.log('CONNECT with no request OR connection\n') } }, subscriptions: { keepAlive: 10000, // 每10秒发送一次Ping onConnect: async (connectionParams, websocket, context) => { console.log('Websocket CONNECTED') let token = connectionParams.authorization || '' const userContext = await createContextFromRequest(context.request, token) // 超时时间设为12秒,比keepAlive多2秒,给网络延迟留缓冲 const PING_TIMEOUT = 12000; let pingTimeoutId; // 重置超时定时器的函数 const resetPingTimeout = () => { if (pingTimeoutId) clearTimeout(pingTimeoutId); pingTimeoutId = setTimeout(() => { // 超时未收到Pong,判定客户端不可达 console.log(`Device ${userContext.deviceId || 'unknown'} is unreachable`); // 这里执行你的数据库更新逻辑,标记设备为离线 // updateDeviceStatus(userContext.deviceId, 'offline'); // 主动终止连接,释放服务器资源 websocket.terminate(); }, PING_TIMEOUT); }; // 首次启动定时器 resetPingTimeout(); // 监听Pong事件,收到后重置定时器 websocket.on('pong', resetPingTimeout); // 将定时器ID存到websocket对象上,方便断开时清理 websocket._pingTimeoutId = pingTimeoutId; return userContext; }, onDisconnect: (websocket, context) => { console.log('Websocket DISCONNECTED\n') // 清理定时器,防止内存泄漏 if (websocket._pingTimeoutId) { clearTimeout(websocket._pingTimeoutId); } // 保留正常断开时的数据库更新逻辑 // updateDeviceStatus(context.deviceId, 'offline'); } } })
关键细节提醒
- 超时时间选择:别和
keepAlive间隔完全一致,留2-3秒缓冲,避免因网络波动误判客户端离线。 - 主动终止连接:调用
websocket.terminate()能确保服务器彻底释放该连接的资源,避免无效连接占用内存。 - 内存泄漏防护:一定要在
onDisconnect中清除定时器,否则已断开连接对应的定时器会持续运行,消耗服务器资源。 - 设备标识关联:确保
userContext中包含设备ID等唯一标识,这样才能准确找到要更新的数据库记录。
这样修改后,不管是客户端正常断开还是异常断网,服务器都能及时检测到并同步更新数据库里的设备连接状态啦。
内容的提问来源于stack exchange,提问作者eric-vinci
相关产品推荐
相关产品推荐

