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 循环的负载进行比较,因为这会减少服务器上的内存压力.