Netty websocket 客户端在空闲 5 分钟后不从服务器读取新帧
Netty websocket client does not read new frames from server after 5 minutes of being idle
我在服务器端和客户端都使用 Netty 来建立和控制 websocket 连接。我在服务器端有一个 IdleStateHandler,它将在通道 reader、编写器或两者空闲一段时间后发送用户事件。我有它,以便在空闲 5 分钟后触发 writer idle 事件,并且在空闲 6 分钟后触发 reader idle 事件。在 writer idle 事件期间,服务器会向客户端发送一个 ping 帧,这将重置 writer 空闲时间以及 reader 空闲时间,一旦从客户端接收到 pong 帧。
问题是 netty 客户端在闲置 5 分钟后似乎没有读取任何新帧。我在客户端的通道上做了一些状态检查,看看它是否可写、已注册、打开以及在 5 分钟的空闲时间后是否处于活动状态,所有状态都是真实的,但没有读取新帧。为了解决这个问题,我只是将服务器端的 IdleStateHandler 时间更改为 3 分钟而不是 5 分钟,以便客户端在空闲 5 分钟之前收到一个 ping 帧并以一个 pong 帧响应。
但这并没有解决根本问题。我想了解并能够控制客户端的 reader 何时空闲,并能够防止将来出现丢失或未读数据的问题。查看下面的代码,如果没有从客户端接收到 pong 或心跳帧,空闲事件处理程序将关闭通道连接,但由于客户端不读取新帧,它永远不会获得关闭帧,因此服务器认为客户端未连接,客户端认为已连接,这显然会导致问题。有没有什么方法可以在客户端使用 Netty 更好地控制这个神奇的 5 分钟超时?我在文档或源代码中找不到任何相关信息。
服务器中相关空闲事件处理代码如下:
private class ConnectServerInitializer extends ChannelInitializer<SocketChannel> {
private final IdleEventHandler idleEventHandler = new IdleEventHandler();
private final SslContext sslCtx;
private ConnectServerInitializer(SslContext sslCtx) {
this.sslCtx = sslCtx;
}
@Override
public void initChannel(SocketChannel ch) throws Exception {
ChannelPipeline pipeline = ch.pipeline();
if (sslCtx != null) {
pipeline.addLast(sslCtx.newHandler(ch.alloc()));
}
pipeline.addLast(new HttpServerCodec());
pipeline.addLast(new HttpObjectAggregator(65536));
pipeline.addLast(idleEventHandler.newStateHandler());
pipeline.addLast(idleEventHandler);
pipeline.addLast(getHandler());
}
}
@Sharable
private class IdleEventHandler extends ChannelDuplexHandler {
private static final String HEARTBEAT_CONTENT = "--heartbeat--";
private static final int READER_IDLE_TIMEOUT = 200; // 20 seconds more that writer to allow for pong response
private static final int WRITER_IDLE_TIMEOUT = 180; // NOTE: netty clients will not read frames after 5 minutes of being idle
// This is a fallback for when clients do not support ping/pong frames
private final AttributeKey<Boolean> USE_HEARTBEAT = AttributeKey.valueOf("use-heartbeat");
@Override
public void userEventTriggered(ChannelHandlerContext ctx, Object event) throws Exception {
if (event instanceof IdleStateEvent) {
IdleStateEvent e = (IdleStateEvent) event;
Boolean useHeartbeat = ctx.attr(USE_HEARTBEAT).get();
if (e.state() == IdleState.READER_IDLE) {
if (useHeartbeat == null) {
logger.info("Client " + ctx.channel() + " has not responded to ping frame. Sending heartbeat message...");
ctx.attr(USE_HEARTBEAT).set(true);
sendHeartbeat(ctx);
} else {
logger.warn("Client " + ctx.channel() + " has been idle for too long. Closing websocket connection...");
ctx.close();
}
} else if (e.state() == IdleState.WRITER_IDLE || e.state() == IdleState.ALL_IDLE) {
if (useHeartbeat == null || !useHeartbeat) {
ByteBuf ping = Unpooled.wrappedBuffer(HEARTBEAT_CONTENT.getBytes());
ctx.writeAndFlush(new PingWebSocketFrame(ping));
} else {
sendHeartbeat(ctx);
}
}
}
}
private void sendHeartbeat(ChannelHandlerContext ctx) {
String json = getHandler().getMessenger().serialize(new HeartbeatMessage(HEARTBEAT_CONTENT));
ctx.writeAndFlush(new TextWebSocketFrame(json));
}
private IdleStateHandler newStateHandler() {
return new IdleStateHandler(READER_IDLE_TIMEOUT, WRITER_IDLE_TIMEOUT, WRITER_IDLE_TIMEOUT);
}
}
您的问题与防火墙超时有关。一些防火墙有接近 5 分钟的超时时间,如果超过此超时时间,连接将自动断开。因此,bot 客户端和服务器需要一些读取超时来检查这个事实,并且服务器、客户端或两者都有某种 ping 消息。当您 运行 通过 IPv6 协议时,防火墙问题会减少,因为大多数 IPv6 防火墙主要是无状态的,通常不会更改连接端口,因此来自客户端的数据包会再次重新激活防火墙中的条目。
当你有很多 5 分钟超时的时刻时,你应该考虑是否可以将来自 websockets 的额外负载与每 1 分钟一次的简单轮询 http 循环的负载进行比较,因为这会减少服务器上的内存压力.
我在服务器端和客户端都使用 Netty 来建立和控制 websocket 连接。我在服务器端有一个 IdleStateHandler,它将在通道 reader、编写器或两者空闲一段时间后发送用户事件。我有它,以便在空闲 5 分钟后触发 writer idle 事件,并且在空闲 6 分钟后触发 reader idle 事件。在 writer idle 事件期间,服务器会向客户端发送一个 ping 帧,这将重置 writer 空闲时间以及 reader 空闲时间,一旦从客户端接收到 pong 帧。
问题是 netty 客户端在闲置 5 分钟后似乎没有读取任何新帧。我在客户端的通道上做了一些状态检查,看看它是否可写、已注册、打开以及在 5 分钟的空闲时间后是否处于活动状态,所有状态都是真实的,但没有读取新帧。为了解决这个问题,我只是将服务器端的 IdleStateHandler 时间更改为 3 分钟而不是 5 分钟,以便客户端在空闲 5 分钟之前收到一个 ping 帧并以一个 pong 帧响应。
但这并没有解决根本问题。我想了解并能够控制客户端的 reader 何时空闲,并能够防止将来出现丢失或未读数据的问题。查看下面的代码,如果没有从客户端接收到 pong 或心跳帧,空闲事件处理程序将关闭通道连接,但由于客户端不读取新帧,它永远不会获得关闭帧,因此服务器认为客户端未连接,客户端认为已连接,这显然会导致问题。有没有什么方法可以在客户端使用 Netty 更好地控制这个神奇的 5 分钟超时?我在文档或源代码中找不到任何相关信息。
服务器中相关空闲事件处理代码如下:
private class ConnectServerInitializer extends ChannelInitializer<SocketChannel> {
private final IdleEventHandler idleEventHandler = new IdleEventHandler();
private final SslContext sslCtx;
private ConnectServerInitializer(SslContext sslCtx) {
this.sslCtx = sslCtx;
}
@Override
public void initChannel(SocketChannel ch) throws Exception {
ChannelPipeline pipeline = ch.pipeline();
if (sslCtx != null) {
pipeline.addLast(sslCtx.newHandler(ch.alloc()));
}
pipeline.addLast(new HttpServerCodec());
pipeline.addLast(new HttpObjectAggregator(65536));
pipeline.addLast(idleEventHandler.newStateHandler());
pipeline.addLast(idleEventHandler);
pipeline.addLast(getHandler());
}
}
@Sharable
private class IdleEventHandler extends ChannelDuplexHandler {
private static final String HEARTBEAT_CONTENT = "--heartbeat--";
private static final int READER_IDLE_TIMEOUT = 200; // 20 seconds more that writer to allow for pong response
private static final int WRITER_IDLE_TIMEOUT = 180; // NOTE: netty clients will not read frames after 5 minutes of being idle
// This is a fallback for when clients do not support ping/pong frames
private final AttributeKey<Boolean> USE_HEARTBEAT = AttributeKey.valueOf("use-heartbeat");
@Override
public void userEventTriggered(ChannelHandlerContext ctx, Object event) throws Exception {
if (event instanceof IdleStateEvent) {
IdleStateEvent e = (IdleStateEvent) event;
Boolean useHeartbeat = ctx.attr(USE_HEARTBEAT).get();
if (e.state() == IdleState.READER_IDLE) {
if (useHeartbeat == null) {
logger.info("Client " + ctx.channel() + " has not responded to ping frame. Sending heartbeat message...");
ctx.attr(USE_HEARTBEAT).set(true);
sendHeartbeat(ctx);
} else {
logger.warn("Client " + ctx.channel() + " has been idle for too long. Closing websocket connection...");
ctx.close();
}
} else if (e.state() == IdleState.WRITER_IDLE || e.state() == IdleState.ALL_IDLE) {
if (useHeartbeat == null || !useHeartbeat) {
ByteBuf ping = Unpooled.wrappedBuffer(HEARTBEAT_CONTENT.getBytes());
ctx.writeAndFlush(new PingWebSocketFrame(ping));
} else {
sendHeartbeat(ctx);
}
}
}
}
private void sendHeartbeat(ChannelHandlerContext ctx) {
String json = getHandler().getMessenger().serialize(new HeartbeatMessage(HEARTBEAT_CONTENT));
ctx.writeAndFlush(new TextWebSocketFrame(json));
}
private IdleStateHandler newStateHandler() {
return new IdleStateHandler(READER_IDLE_TIMEOUT, WRITER_IDLE_TIMEOUT, WRITER_IDLE_TIMEOUT);
}
}
您的问题与防火墙超时有关。一些防火墙有接近 5 分钟的超时时间,如果超过此超时时间,连接将自动断开。因此,bot 客户端和服务器需要一些读取超时来检查这个事实,并且服务器、客户端或两者都有某种 ping 消息。当您 运行 通过 IPv6 协议时,防火墙问题会减少,因为大多数 IPv6 防火墙主要是无状态的,通常不会更改连接端口,因此来自客户端的数据包会再次重新激活防火墙中的条目。
当你有很多 5 分钟超时的时刻时,你应该考虑是否可以将来自 websockets 的额外负载与每 1 分钟一次的简单轮询 http 循环的负载进行比较,因为这会减少服务器上的内存压力.