NettyでIOException:Connection reset by peerとjava.nio.channels.ClosedChannelException: null
17439 ワード
最近、多くのIOException:Connection reset by peerとClosedChannelException:nullが発見されました
コードを深く見て、いくつかのテストをして、Connection resetがクライアントがchannelが閉じられていることを知らない場合、eventloopのunsafeをトリガーすることを発見しました.read()操作放出
一方、ClosedChannelExceptionは一般的にNettyによって自発的に投げ出されており、AbstractChannelやSSLhandlerではClosedChannel関連のコードが見られます
AbstractChannel
コードの多くの部分では、このClosedChannelExceptionがあります.つまり、channel close以降にwriteメソッドが呼び出されると、writeのfutureがfailureに設定され、causeがClosedChannelExceptionに設定されます.同じSSLhandlerでも似ています.
-----------------
Connection reset by peerに戻ると、この状況をシミュレートするのは簡単です.サーバ側にchannelActiveのときにclose channelのhandlerを設定します.クライアント側ではConnectに成功した直後にリクエストデータを送信するlistenerを書く.次のように
client
server
この場合connection reset by peer異常をトリガーできるのは、connectが成功するとclientセグメントが先にconnectに成功したlistenerをトリガーするためで、このときserverセグメントはchannelを切断していますが、channelが切断されたイベントもトリガーされます(クライアントreadイベントがトリガーされますが、このreadは-1を返します.-1はchannelが閉鎖されたことを表し、clientのchannelInactiveとchannel activeの状態の変化はこの時に発生しました).しかし、このイベントはconnectに成功したlistenerの後に実行されるので、この時listenerのchannelは自分が切断されたことを知らないので、writeとflushの操作を続け、flushを呼び出すとeventloopがOP_に入ります.READ事件の時read()はconnection reset異常を投げ出す.eventloopコードは次のとおりです.
NioEventLoop
これがconnection reset by peerの発生の原因です
------------------
ClosedChannelExceptionがどのように生成されたかを見てみると、彼を再現するのも簡単です.まず、クライアントがアクティブに閉じてからClosedChannelExceptionが現れるわけではないことを明確にしなければならない.次に、ClosedChannelExceptionが表示されるクライアントの書き方を2つ見てみましょう.
クライアント1、チャンネルをアクティブに閉じる
writeの前にcloseをアクティブに呼び出すと、writeは必ずcloseがclose状態であることを知っていて、最後にwriteは失敗して、futureの中のcauseはClosedChannelExceptionです
--------------------
client 2. サービス側によるClosedChannelException
サービス側
この場合、サービス側はchannelを閉じ、クライアントはsleepを先に閉じ、この間clientのeventLoopはクライアントが閉じた時間、すなわちeventLoopのprocessKeyメソッドがOP_に入るREAD、それからreadは1つ出てきて、最後にclient channelInactiveイベントをトリガーして、sleepが目が覚めると、クライアントはwriteAndFlushを呼び出して、この時クライアントchannelの状態はすでにinactiveになって、だからwriteは失敗して、causeはClosedChannelExceptionです
コードを深く見て、いくつかのテストをして、Connection resetがクライアントがchannelが閉じられていることを知らない場合、eventloopのunsafeをトリガーすることを発見しました.read()操作放出
一方、ClosedChannelExceptionは一般的にNettyによって自発的に投げ出されており、AbstractChannelやSSLhandlerではClosedChannel関連のコードが見られます
AbstractChannel
static final ClosedChannelException CLOSED_CHANNEL_EXCEPTION = new ClosedChannelException();
...
static {
CLOSED_CHANNEL_EXCEPTION.setStackTrace(EmptyArrays.EMPTY_STACK_TRACE);
NOT_YET_CONNECTED_EXCEPTION.setStackTrace(EmptyArrays.EMPTY_STACK_TRACE);
}
...
@Override
public void write(Object msg, ChannelPromise promise) {
ChannelOutboundBuffer outboundBuffer = this.outboundBuffer;
if (outboundBuffer == null) {
// If the outboundBuffer is null we know the channel was closed and so
// need to fail the future right away. If it is not null the handling of the rest
// will be done in flush0()
// See https://github.com/netty/netty/issues/2362
safeSetFailure(promise, CLOSED_CHANNEL_EXCEPTION);
// release message now to prevent resource-leak
ReferenceCountUtil.release(msg);
return;
}
outboundBuffer.addMessage(msg, promise);
}
コードの多くの部分では、このClosedChannelExceptionがあります.つまり、channel close以降にwriteメソッドが呼び出されると、writeのfutureがfailureに設定され、causeがClosedChannelExceptionに設定されます.同じSSLhandlerでも似ています.
-----------------
Connection reset by peerに戻ると、この状況をシミュレートするのは簡単です.サーバ側にchannelActiveのときにclose channelのhandlerを設定します.クライアント側ではConnectに成功した直後にリクエストデータを送信するlistenerを書く.次のように
client
public static void main(String[] args) throws IOException, InterruptedException {
Bootstrap b = new Bootstrap();
b.group(new NioEventLoopGroup())
.channel(NioSocketChannel.class)
.handler(new ChannelInitializer<NioSocketChannel>() {
@Override
protected void initChannel(NioSocketChannel ch) throws Exception {
}
});
b.connect("localhost", 8090).addListener(new ChannelFutureListener() {
@Override
public void operationComplete(ChannelFuture future) throws Exception {
if (future.isSuccess()) {
future.channel().write(Unpooled.buffer().writeBytes("123".getBytes()));
future.channel().flush();
}
}
});
server
public class SimpleServer {
public static void main(String[] args) throws Exception {
EventLoopGroup bossGroup = new NioEventLoopGroup(1);
EventLoopGroup workerGroup = new NioEventLoopGroup();
ServerBootstrap b = new ServerBootstrap();
b.group(bossGroup, workerGroup)
.channel(NioServerSocketChannel.class)
.option(ChannelOption.SO_REUSEADDR, true)
.childHandler(new ChannelInitializer<NioSocketChannel>() {
@Override
protected void initChannel(NioSocketChannel ch) throws Exception {
ch.pipeline().addLast(new SimpleServerHandler());
}
});
b.bind(8090).sync().channel().closeFuture().sync();
}
}
public class SimpleServerHandler extends ChannelInboundHandlerAdapter {
@Override
public void channelActive(ChannelHandlerContext ctx) throws Exception {
ctx.channel().close().sync();
}
@Override
public void channelRead(ChannelHandlerContext ctx, final Object msg) throws Exception {
System.out.println(123);
}
@Override
public void channelInactive(ChannelHandlerContext ctx) throws Exception {
System.out.println("inactive");
}
}
この場合connection reset by peer異常をトリガーできるのは、connectが成功するとclientセグメントが先にconnectに成功したlistenerをトリガーするためで、このときserverセグメントはchannelを切断していますが、channelが切断されたイベントもトリガーされます(クライアントreadイベントがトリガーされますが、このreadは-1を返します.-1はchannelが閉鎖されたことを表し、clientのchannelInactiveとchannel activeの状態の変化はこの時に発生しました).しかし、このイベントはconnectに成功したlistenerの後に実行されるので、この時listenerのchannelは自分が切断されたことを知らないので、writeとflushの操作を続け、flushを呼び出すとeventloopがOP_に入ります.READ事件の時read()はconnection reset異常を投げ出す.eventloopコードは次のとおりです.
NioEventLoop
private static void processSelectedKey(SelectionKey k, AbstractNioChannel ch) {
final NioUnsafe unsafe = ch.unsafe();
if (!k.isValid()) {
// close the channel if the key is not valid anymore
unsafe.close(unsafe.voidPromise());
return;
}
try {
int readyOps = k.readyOps();
// Also check for readOps of 0 to workaround possible JDK bug which may otherwise lead
// to a spin loop
if ((readyOps & (SelectionKey.OP_READ | SelectionKey.OP_ACCEPT)) != 0 || readyOps == 0) {
unsafe.read(); if (!ch.isOpen()) {
// Connection already closed - no need to handle write.
return;
}
}
if ((readyOps & SelectionKey.OP_WRITE) != 0) {
// Call forceFlush which will also take care of clear the OP_WRITE once there is nothing left to write
ch.unsafe().forceFlush();
}
if ((readyOps & SelectionKey.OP_CONNECT) != 0) {
// remove OP_CONNECT as otherwise Selector.select(..) will always return without blocking
// See https://github.com/netty/netty/issues/924
int ops = k.interestOps();
ops &= ~SelectionKey.OP_CONNECT;
k.interestOps(ops);
unsafe.finishConnect();
}
} catch (CancelledKeyException e) {
unsafe.close(unsafe.voidPromise());
}
}
これがconnection reset by peerの発生の原因です
------------------
ClosedChannelExceptionがどのように生成されたかを見てみると、彼を再現するのも簡単です.まず、クライアントがアクティブに閉じてからClosedChannelExceptionが現れるわけではないことを明確にしなければならない.次に、ClosedChannelExceptionが表示されるクライアントの書き方を2つ見てみましょう.
クライアント1、チャンネルをアクティブに閉じる
public class SimpleClient {
private static final Logger logger = LoggerFactory.getLogger(SimpleClient.class);
public static void main(String[] args) throws IOException, InterruptedException {
Bootstrap b = new Bootstrap();
b.group(new NioEventLoopGroup())
.channel(NioSocketChannel.class)
.handler(new ChannelInitializer<NioSocketChannel>() {
@Override
protected void initChannel(NioSocketChannel ch) throws Exception {
}
});
b.connect("localhost", 8090).addListener(new ChannelFutureListener() {
@Override
public void operationComplete(ChannelFuture future) throws Exception {
if (future.isSuccess()) {
future.channel().close();
future.channel().write(Unpooled.buffer().writeBytes("123".getBytes())).addListener(new ChannelFutureListener() {
@Override
public void operationComplete(ChannelFuture future) throws Exception {
if (!future.isSuccess()) {
logger.error("Error", future.cause());
}
}
});
future.channel().flush();
}
}
});
}
}
writeの前にcloseをアクティブに呼び出すと、writeは必ずcloseがclose状態であることを知っていて、最後にwriteは失敗して、futureの中のcauseはClosedChannelExceptionです
--------------------
client 2. サービス側によるClosedChannelException
public class SimpleClient {
private static final Logger logger = LoggerFactory.getLogger(SimpleClient.class);
public static void main(String[] args) throws IOException, InterruptedException {
Bootstrap b = new Bootstrap();
b.group(new NioEventLoopGroup())
.channel(NioSocketChannel.class)
.handler(new ChannelInitializer<NioSocketChannel>() {
@Override
protected void initChannel(NioSocketChannel ch) throws Exception {
}
});
Channel channel = b.connect("localhost", 8090).sync().channel();
Thread.sleep(3000);
channel.writeAndFlush(Unpooled.buffer().writeBytes("123".getBytes())).addListener(new ChannelFutureListener() {
@Override
public void operationComplete(ChannelFuture future) throws Exception {
if (!future.isSuccess()) {
logger.error("error", future.cause());
}
}
});
}
}
サービス側
public class SimpleServer {
public static void main(String[] args) throws Exception {
EventLoopGroup bossGroup = new NioEventLoopGroup(1);
EventLoopGroup workerGroup = new NioEventLoopGroup();
ServerBootstrap b = new ServerBootstrap();
b.group(bossGroup, workerGroup)
.channel(NioServerSocketChannel.class)
.option(ChannelOption.SO_REUSEADDR, true)
.childHandler(new ChannelInitializer<NioSocketChannel>() {
@Override
protected void initChannel(NioSocketChannel ch) throws Exception {
ch.pipeline().addLast(new SimpleServerHandler());
}
});
b.bind(8090).sync().channel().closeFuture().sync();
}
}
この場合、サービス側はchannelを閉じ、クライアントはsleepを先に閉じ、この間clientのeventLoopはクライアントが閉じた時間、すなわちeventLoopのprocessKeyメソッドがOP_に入るREAD、それからreadは1つ出てきて、最後にclient channelInactiveイベントをトリガーして、sleepが目が覚めると、クライアントはwriteAndFlushを呼び出して、この時クライアントchannelの状態はすでにinactiveになって、だからwriteは失敗して、causeはClosedChannelExceptionです