You need to enable JavaScript to run this app.
优惠活动
大模型
产品
解决方案
定价
更多

Netty客户端关闭连接时偶现Connection reset by peer异常求助

问题

使用Netty开发WebSocket服务端与客户端,已重写channelInactive和exceptionCaught方法。客户端关闭连接时,服务端偶尔抛出java.io.IOException: Connection reset by peer异常。推测该异常是服务端尝试从已关闭的通道读取数据导致,怀疑客户端调用channel.close()时最后一条消息的IO操作未完成,但即使在发送最后一条消息与关闭操作间添加1秒延迟(本地运行),异常仍会出现。请问如何消除该异常,还是只需忽略?

相关代码

服务端Handler代码:

@Override
public void channelInactive(ChannelHandlerContext ctx) throws Exception {
    wsHolder.remove(ctx);

    List<ChannelHandlerContext> ctxList = wsHolder.getAllUserCtx();
    if (ctxList.size() > 0) {
        for (ChannelHandlerContext octx : ctxList) {
            if (!octx.channel().id().equals(ctx.channel().id())) {
                octx.channel().writeAndFlush(new UserLogout(id));
            }
        }
    }
    super.channelInactive(ctx);
}

@Override
public void exceptionCaught(ChannelHandlerContext ctx, Throwable cause) {
    log.error("Exception in WebsocketInHandler of ctx {}: {}", wsHolder.getUserId(ctx), cause.getMessage());
    log.error("Error:", cause);
    if (!(ctx.channel().isActive() && ctx.channel().isWritable())) {
        ctx.close();
    }
}

异常信息

java.io.IOException: Connection reset by peer
    at sun.nio.ch.FileDispatcherImpl.read0(Native Method)
    at sun.nio.ch.SocketDispatcher.read(SocketDispatcher.java:39)
    at sun.nio.ch.IOUtil.readIntoNativeBuffer(IOUtil.java:223)
    at sun.nio.ch.IOUtil.read(IOUtil.java:192)
    at sun.nio.ch.SocketChannelImpl.read(SocketChannelImpl.java:380)
    at io.netty.buffer.PooledByteBuf.setBytes(PooledByteBuf.java:253)
    at io.netty.buffer.AbstractByteBuf.writeBytes(AbstractByteBuf.java:1133)
    at io.netty.channel.socket.nio.NioSocketChannel.doReadBytes(NioSocketChannel.java:350)
    at io.netty.channel.nio.AbstractNioByteChannel$NioByteUnsafe.read(AbstractNioByteChannel.java:148)
    at io.netty.channel.nio.NioEventLoop.processSelectedKey(NioEventLoop.java:714)
    at io.netty.channel.nio.NioEventLoop.processSelectedKeysOptimized(NioEventLoop.java:650)
    at io.netty.channel.nio.NioEventLoop.processSelectedKeys(NioEventLoop.java:576)
    at io.netty.channel.nio.NioEventLoop.run(NioEventLoop.java:493)
    at io.netty.util.concurrent.SingleThreadEventExecutor$4.run(SingleThreadEventExecutor.java:989)
    at io.netty.util.internal.ThreadExecutorMap$2.run(ThreadExecutorMap.java:74)
    at io.netty.util.concurrent.FastThreadLocalRunnable.run(FastThreadLocalRunnable.java:30)
    at java.lang.Thread.run(Thread.java:748)
原因分析

这个异常本质是TCP连接层面的重置,和最后一条消息是否完成IO操作无关。核心原因是客户端没有遵循WebSocket规范的关闭流程:直接调用channel.close()会强制关闭TCP连接,此时服务端的NIO线程可能还在尝试从通道读取数据,触发Connection reset by peer异常。延迟等待无法解决问题,因为Netty的IO操作是异步的,固定延迟无法保证所有异步操作都已完成。

解决方案

1. 遵循WebSocket规范的关闭流程(推荐)

WebSocket协议定义了标准的关闭握手流程,从根源上避免异常:

  • 客户端先发送CloseWebSocketFrame给服务端
  • 服务端收到后,回应一个CloseWebSocketFrame
  • 双方收到对方的Close帧后,再关闭通道

客户端关闭示例代码:

// 发送标准Close帧,指定关闭状态码和原因
CloseWebSocketFrame closeFrame = new CloseWebSocketFrame(1000, "Normal closure");
channel.writeAndFlush(closeFrame).addListener(future -> {
    if (future.isSuccess()) {
        // 等待服务端回应Close帧后,Netty会自动触发channelInactive,无需手动关闭
    }
});

服务端处理Close帧的示例:
在WebSocketHandler中添加对Close帧的处理:

@Override
protected void channelRead0(ChannelHandlerContext ctx, WebSocketFrame frame) throws Exception {
    if (frame instanceof CloseWebSocketFrame) {
        // 回应Close帧
        ctx.writeAndFlush(((CloseWebSocketFrame) frame).retain());
        // 关闭通道
        ctx.close();
        return;
    }
    // 处理其他类型的WebSocket帧...
}

2. 优化exceptionCaught方法,过滤该异常

如果暂时无法修改客户端关闭流程,可以在服务端的exceptionCaught中识别该异常,降低日志级别或直接忽略,避免不必要的错误日志:

@Override
public void exceptionCaught(ChannelHandlerContext ctx, Throwable cause) {
    // 识别Connection reset by peer异常
    if (cause instanceof IOException && "Connection reset by peer".equals(cause.getMessage())) {
        // 记录debug级别的日志即可,无需输出完整栈信息
        log.debug("Connection reset by peer for user: {}", wsHolder.getUserId(ctx));
    } else {
        log.error("Exception in WebsocketInHandler of ctx {}: {}", wsHolder.getUserId(ctx), cause.getMessage());
        log.error("Error:", cause);
    }
    // 通道已经不可用,直接关闭即可,无需额外状态判断
    ctx.close();
}

3. 确保channelInactive中无残留IO操作

检查channelInactive中的逻辑,确保不会对已关闭的通道进行任何IO操作。当前代码中遍历其他通道发送UserLogout是合理的,但要确保目标通道状态正常。

总结

优先通过遵循WebSocket规范的关闭流程来消除异常,这是最规范的解决方案。如果无法修改客户端,优化exceptionCaught方法过滤该异常即可——这种异常属于网络连接关闭时的正常现象,不会影响服务稳定性,可以安全忽略(建议保留debug日志以便排查问题)。

内容的提问来源于stack exchange,提问作者leetom

相关产品推荐
方舟 Agent Plan

超全模态模型 × Harness 升级,最新支持 Deepseek-V4.1-Flash、GLM-5.3 系列、Doubao-Seedream-5.0-pro、Kimi-K3 (部分), 限时 9.9 元起

最近更新时间:2026.08.14 03:20:35