b57d9f307f
- write() now accepts a ChannelPromise and returns ChannelFuture as most users expected. It makes the user's life much easier because it is now much easier to get notified when a specific message has been written. - flush() does not create a ChannelPromise nor returns ChannelFuture. It is now similar to what read() looks like.
442 lines
16 KiB
Java
442 lines
16 KiB
Java
/*
|
|
* Copyright 2012 The Netty Project
|
|
*
|
|
* The Netty Project licenses this file to you under the Apache License,
|
|
* version 2.0 (the "License"); you may not use this file except in compliance
|
|
* with the License. You may obtain a copy of the License at:
|
|
*
|
|
* http://www.apache.org/licenses/LICENSE-2.0
|
|
*
|
|
* Unless required by applicable law or agreed to in writing, software
|
|
* distributed under the License is distributed on an "AS IS" BASIS, WITHOUT
|
|
* WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the
|
|
* License for the specific language governing permissions and limitations
|
|
* under the License.
|
|
*/
|
|
package io.netty.handler.timeout;
|
|
|
|
import io.netty.bootstrap.ServerBootstrap;
|
|
import io.netty.channel.Channel;
|
|
import io.netty.channel.ChannelDuplexHandler;
|
|
import io.netty.channel.ChannelFuture;
|
|
import io.netty.channel.ChannelFutureListener;
|
|
import io.netty.channel.ChannelHandlerContext;
|
|
import io.netty.channel.ChannelInitializer;
|
|
import io.netty.channel.ChannelPromise;
|
|
import io.netty.util.concurrent.EventExecutor;
|
|
|
|
import java.util.concurrent.ScheduledFuture;
|
|
import java.util.concurrent.TimeUnit;
|
|
|
|
/**
|
|
* Triggers an {@link IdleStateEvent} when a {@link Channel} has not performed
|
|
* read, write, or both operation for a while.
|
|
*
|
|
* <h3>Supported idle states</h3>
|
|
* <table border="1">
|
|
* <tr>
|
|
* <th>Property</th><th>Meaning</th>
|
|
* </tr>
|
|
* <tr>
|
|
* <td>{@code readerIdleTime}</td>
|
|
* <td>an {@link IdleStateEvent} whose state is {@link IdleState#READER_IDLE}
|
|
* will be triggered when no read was performed for the specified period of
|
|
* time. Specify {@code 0} to disable.</td>
|
|
* </tr>
|
|
* <tr>
|
|
* <td>{@code writerIdleTime}</td>
|
|
* <td>an {@link IdleStateEvent} whose state is {@link IdleState#WRITER_IDLE}
|
|
* will be triggered when no write was performed for the specified period of
|
|
* time. Specify {@code 0} to disable.</td>
|
|
* </tr>
|
|
* <tr>
|
|
* <td>{@code allIdleTime}</td>
|
|
* <td>an {@link IdleStateEvent} whose state is {@link IdleState#ALL_IDLE}
|
|
* will be triggered when neither read nor write was performed for the
|
|
* specified period of time. Specify {@code 0} to disable.</td>
|
|
* </tr>
|
|
* </table>
|
|
*
|
|
* <pre>
|
|
* // An example that sends a ping message when there is no outbound traffic
|
|
* // for 30 seconds. The connection is closed when there is no inbound traffic
|
|
* // for 60 seconds.
|
|
*
|
|
* public class MyChannelInitializer extends {@link ChannelInitializer}<{@link Channel}> {
|
|
* {@code @Override}
|
|
* public void initChannel({@link Channel} channel) {
|
|
* channel.pipeline().addLast("idleStateHandler", new {@link IdleStateHandler}(60, 30, 0);
|
|
* channel.pipeline().addLast("myHandler", new MyHandler());
|
|
* }
|
|
* }
|
|
*
|
|
* // Handler should handle the {@link IdleStateEvent} triggered by {@link IdleStateHandler}.
|
|
* public class MyHandler extends {@link ChannelDuplexHandler} {
|
|
* {@code @Override}
|
|
* public void userEventTriggered({@link ChannelHandlerContext} ctx, {@link Object} evt) throws {@link Exception} {
|
|
* if (evt instanceof {@link IdleState}} {
|
|
* {@link IdleState} e = ({@link IdleState}) evt;
|
|
* if (e.getState() == {@link IdleState}.READER_IDLE) {
|
|
* ctx.channel().close();
|
|
* } else if (e.getState() == {@link IdleState}.WRITER_IDLE) {
|
|
* ctx.channel().write(new PingMessage());
|
|
* }
|
|
* }
|
|
* }
|
|
* }
|
|
*
|
|
* {@link ServerBootstrap} bootstrap = ...;
|
|
* ...
|
|
* bootstrap.childHandler(new MyChannelInitializer());
|
|
* ...
|
|
* </pre>
|
|
*
|
|
* @see ReadTimeoutHandler
|
|
* @see WriteTimeoutHandler
|
|
*/
|
|
public class IdleStateHandler extends ChannelDuplexHandler {
|
|
|
|
private final long readerIdleTimeMillis;
|
|
private final long writerIdleTimeMillis;
|
|
private final long allIdleTimeMillis;
|
|
|
|
volatile ScheduledFuture<?> readerIdleTimeout;
|
|
volatile long lastReadTime;
|
|
private boolean firstReaderIdleEvent = true;
|
|
|
|
volatile ScheduledFuture<?> writerIdleTimeout;
|
|
volatile long lastWriteTime;
|
|
private boolean firstWriterIdleEvent = true;
|
|
|
|
volatile ScheduledFuture<?> allIdleTimeout;
|
|
private boolean firstAllIdleEvent = true;
|
|
|
|
private volatile int state; // 0 - none, 1 - initialized, 2 - destroyed
|
|
|
|
/**
|
|
* Creates a new instance firing {@link IdleStateEvent}s.
|
|
*
|
|
* @param readerIdleTimeSeconds
|
|
* an {@link IdleStateEvent} whose state is {@link IdleState#READER_IDLE}
|
|
* will be triggered when no read was performed for the specified
|
|
* period of time. Specify {@code 0} to disable.
|
|
* @param writerIdleTimeSeconds
|
|
* an {@link IdleStateEvent} whose state is {@link IdleState#WRITER_IDLE}
|
|
* will be triggered when no write was performed for the specified
|
|
* period of time. Specify {@code 0} to disable.
|
|
* @param allIdleTimeSeconds
|
|
* an {@link IdleStateEvent} whose state is {@link IdleState#ALL_IDLE}
|
|
* will be triggered when neither read nor write was performed for
|
|
* the specified period of time. Specify {@code 0} to disable.
|
|
*/
|
|
public IdleStateHandler(
|
|
int readerIdleTimeSeconds,
|
|
int writerIdleTimeSeconds,
|
|
int allIdleTimeSeconds) {
|
|
|
|
this(readerIdleTimeSeconds, writerIdleTimeSeconds, allIdleTimeSeconds,
|
|
TimeUnit.SECONDS);
|
|
}
|
|
|
|
/**
|
|
* Creates a new instance firing {@link IdleStateEvent}s.
|
|
*
|
|
* @param readerIdleTime
|
|
* an {@link IdleStateEvent} whose state is {@link IdleState#READER_IDLE}
|
|
* will be triggered when no read was performed for the specified
|
|
* period of time. Specify {@code 0} to disable.
|
|
* @param writerIdleTime
|
|
* an {@link IdleStateEvent} whose state is {@link IdleState#WRITER_IDLE}
|
|
* will be triggered when no write was performed for the specified
|
|
* period of time. Specify {@code 0} to disable.
|
|
* @param allIdleTime
|
|
* an {@link IdleStateEvent} whose state is {@link IdleState#ALL_IDLE}
|
|
* will be triggered when neither read nor write was performed for
|
|
* the specified period of time. Specify {@code 0} to disable.
|
|
* @param unit
|
|
* the {@link TimeUnit} of {@code readerIdleTime},
|
|
* {@code writeIdleTime}, and {@code allIdleTime}
|
|
*/
|
|
public IdleStateHandler(
|
|
long readerIdleTime, long writerIdleTime, long allIdleTime,
|
|
TimeUnit unit) {
|
|
if (unit == null) {
|
|
throw new NullPointerException("unit");
|
|
}
|
|
|
|
if (readerIdleTime <= 0) {
|
|
readerIdleTimeMillis = 0;
|
|
} else {
|
|
readerIdleTimeMillis = Math.max(unit.toMillis(readerIdleTime), 1);
|
|
}
|
|
if (writerIdleTime <= 0) {
|
|
writerIdleTimeMillis = 0;
|
|
} else {
|
|
writerIdleTimeMillis = Math.max(unit.toMillis(writerIdleTime), 1);
|
|
}
|
|
if (allIdleTime <= 0) {
|
|
allIdleTimeMillis = 0;
|
|
} else {
|
|
allIdleTimeMillis = Math.max(unit.toMillis(allIdleTime), 1);
|
|
}
|
|
}
|
|
|
|
/**
|
|
* Return the readerIdleTime that was given when instance this class in milliseconds.
|
|
*
|
|
*/
|
|
public long getReaderIdleTimeInMillis() {
|
|
return readerIdleTimeMillis;
|
|
}
|
|
|
|
/**
|
|
* Return the writerIdleTime that was given when instance this class in milliseconds.
|
|
*
|
|
*/
|
|
public long getWriterIdleTimeInMillis() {
|
|
return writerIdleTimeMillis;
|
|
}
|
|
|
|
/**
|
|
* Return the allIdleTime that was given when instance this class in milliseconds.
|
|
*
|
|
*/
|
|
public long getAllIdleTimeInMillis() {
|
|
return allIdleTimeMillis;
|
|
}
|
|
|
|
@Override
|
|
public void handlerAdded(ChannelHandlerContext ctx) throws Exception {
|
|
if (ctx.channel().isActive() & ctx.channel().isRegistered()) {
|
|
// channelActvie() event has been fired already, which means this.channelActive() will
|
|
// not be invoked. We have to initialize here instead.
|
|
initialize(ctx);
|
|
} else {
|
|
// channelActive() event has not been fired yet. this.channelActive() will be invoked
|
|
// and initialization will occur there.
|
|
}
|
|
}
|
|
|
|
@Override
|
|
public void handlerRemoved(ChannelHandlerContext ctx) throws Exception {
|
|
destroy();
|
|
}
|
|
|
|
@Override
|
|
public void channelRegistered(ChannelHandlerContext ctx) throws Exception {
|
|
// Initialize early if channel is active already.
|
|
if (ctx.channel().isActive()) {
|
|
initialize(ctx);
|
|
}
|
|
super.channelRegistered(ctx);
|
|
}
|
|
|
|
@Override
|
|
public void channelActive(ChannelHandlerContext ctx) throws Exception {
|
|
// This method will be invoked only if this handler was added
|
|
// before channelActive() event is fired. If a user adds this handler
|
|
// after the channelActive() event, initialize() will be called by beforeAdd().
|
|
initialize(ctx);
|
|
super.channelActive(ctx);
|
|
}
|
|
|
|
@Override
|
|
public void channelInactive(ChannelHandlerContext ctx) throws Exception {
|
|
destroy();
|
|
super.channelInactive(ctx);
|
|
}
|
|
|
|
@Override
|
|
public void channelRead(ChannelHandlerContext ctx, Object msg) throws Exception {
|
|
lastReadTime = System.currentTimeMillis();
|
|
firstReaderIdleEvent = firstAllIdleEvent = true;
|
|
ctx.fireChannelRead(msg);
|
|
}
|
|
|
|
@Override
|
|
public void write(ChannelHandlerContext ctx, Object msg, ChannelPromise promise) throws Exception {
|
|
promise.addListener(new ChannelFutureListener() {
|
|
@Override
|
|
public void operationComplete(ChannelFuture future) throws Exception {
|
|
lastWriteTime = System.currentTimeMillis();
|
|
firstWriterIdleEvent = firstAllIdleEvent = true;
|
|
}
|
|
});
|
|
ctx.write(msg, promise);
|
|
}
|
|
|
|
private void initialize(ChannelHandlerContext ctx) {
|
|
// Avoid the case where destroy() is called before scheduling timeouts.
|
|
// See: https://github.com/netty/netty/issues/143
|
|
switch (state) {
|
|
case 1:
|
|
case 2:
|
|
return;
|
|
}
|
|
|
|
state = 1;
|
|
|
|
EventExecutor loop = ctx.executor();
|
|
|
|
lastReadTime = lastWriteTime = System.currentTimeMillis();
|
|
if (readerIdleTimeMillis > 0) {
|
|
readerIdleTimeout = loop.schedule(
|
|
new ReaderIdleTimeoutTask(ctx),
|
|
readerIdleTimeMillis, TimeUnit.MILLISECONDS);
|
|
}
|
|
if (writerIdleTimeMillis > 0) {
|
|
writerIdleTimeout = loop.schedule(
|
|
new WriterIdleTimeoutTask(ctx),
|
|
writerIdleTimeMillis, TimeUnit.MILLISECONDS);
|
|
}
|
|
if (allIdleTimeMillis > 0) {
|
|
allIdleTimeout = loop.schedule(
|
|
new AllIdleTimeoutTask(ctx),
|
|
allIdleTimeMillis, TimeUnit.MILLISECONDS);
|
|
}
|
|
}
|
|
|
|
private void destroy() {
|
|
state = 2;
|
|
|
|
if (readerIdleTimeout != null) {
|
|
readerIdleTimeout.cancel(false);
|
|
readerIdleTimeout = null;
|
|
}
|
|
if (writerIdleTimeout != null) {
|
|
writerIdleTimeout.cancel(false);
|
|
writerIdleTimeout = null;
|
|
}
|
|
if (allIdleTimeout != null) {
|
|
allIdleTimeout.cancel(false);
|
|
allIdleTimeout = null;
|
|
}
|
|
}
|
|
|
|
protected void channelIdle(ChannelHandlerContext ctx, IdleStateEvent evt) throws Exception {
|
|
ctx.fireUserEventTriggered(evt);
|
|
}
|
|
|
|
private final class ReaderIdleTimeoutTask implements Runnable {
|
|
|
|
private final ChannelHandlerContext ctx;
|
|
|
|
ReaderIdleTimeoutTask(ChannelHandlerContext ctx) {
|
|
this.ctx = ctx;
|
|
}
|
|
|
|
@Override
|
|
public void run() {
|
|
if (!ctx.channel().isOpen()) {
|
|
return;
|
|
}
|
|
|
|
long currentTime = System.currentTimeMillis();
|
|
long lastReadTime = IdleStateHandler.this.lastReadTime;
|
|
long nextDelay = readerIdleTimeMillis - (currentTime - lastReadTime);
|
|
if (nextDelay <= 0) {
|
|
// Reader is idle - set a new timeout and notify the callback.
|
|
readerIdleTimeout =
|
|
ctx.executor().schedule(this, readerIdleTimeMillis, TimeUnit.MILLISECONDS);
|
|
try {
|
|
IdleStateEvent event;
|
|
if (firstReaderIdleEvent) {
|
|
firstReaderIdleEvent = false;
|
|
event = IdleStateEvent.FIRST_READER_IDLE_STATE_EVENT;
|
|
} else {
|
|
event = IdleStateEvent.READER_IDLE_STATE_EVENT;
|
|
}
|
|
channelIdle(ctx, event);
|
|
} catch (Throwable t) {
|
|
ctx.fireExceptionCaught(t);
|
|
}
|
|
} else {
|
|
// Read occurred before the timeout - set a new timeout with shorter delay.
|
|
readerIdleTimeout = ctx.executor().schedule(this, nextDelay, TimeUnit.MILLISECONDS);
|
|
}
|
|
}
|
|
}
|
|
|
|
private final class WriterIdleTimeoutTask implements Runnable {
|
|
|
|
private final ChannelHandlerContext ctx;
|
|
|
|
WriterIdleTimeoutTask(ChannelHandlerContext ctx) {
|
|
this.ctx = ctx;
|
|
}
|
|
|
|
@Override
|
|
public void run() {
|
|
if (!ctx.channel().isOpen()) {
|
|
return;
|
|
}
|
|
|
|
long currentTime = System.currentTimeMillis();
|
|
long lastWriteTime = IdleStateHandler.this.lastWriteTime;
|
|
long nextDelay = writerIdleTimeMillis - (currentTime - lastWriteTime);
|
|
if (nextDelay <= 0) {
|
|
// Writer is idle - set a new timeout and notify the callback.
|
|
writerIdleTimeout = ctx.executor().schedule(
|
|
this, writerIdleTimeMillis, TimeUnit.MILLISECONDS);
|
|
try {
|
|
IdleStateEvent event;
|
|
if (firstWriterIdleEvent) {
|
|
firstWriterIdleEvent = false;
|
|
event = IdleStateEvent.FIRST_WRITER_IDLE_STATE_EVENT;
|
|
} else {
|
|
event = IdleStateEvent.WRITER_IDLE_STATE_EVENT;
|
|
}
|
|
channelIdle(ctx, event);
|
|
} catch (Throwable t) {
|
|
ctx.fireExceptionCaught(t);
|
|
}
|
|
} else {
|
|
// Write occurred before the timeout - set a new timeout with shorter delay.
|
|
writerIdleTimeout = ctx.executor().schedule(this, nextDelay, TimeUnit.MILLISECONDS);
|
|
}
|
|
}
|
|
}
|
|
|
|
private final class AllIdleTimeoutTask implements Runnable {
|
|
|
|
private final ChannelHandlerContext ctx;
|
|
|
|
AllIdleTimeoutTask(ChannelHandlerContext ctx) {
|
|
this.ctx = ctx;
|
|
}
|
|
|
|
@Override
|
|
public void run() {
|
|
if (!ctx.channel().isOpen()) {
|
|
return;
|
|
}
|
|
|
|
long currentTime = System.currentTimeMillis();
|
|
long lastIoTime = Math.max(lastReadTime, lastWriteTime);
|
|
long nextDelay = allIdleTimeMillis - (currentTime - lastIoTime);
|
|
if (nextDelay <= 0) {
|
|
// Both reader and writer are idle - set a new timeout and
|
|
// notify the callback.
|
|
allIdleTimeout = ctx.executor().schedule(
|
|
this, allIdleTimeMillis, TimeUnit.MILLISECONDS);
|
|
try {
|
|
IdleStateEvent event;
|
|
if (firstAllIdleEvent) {
|
|
firstAllIdleEvent = false;
|
|
event = IdleStateEvent.FIRST_ALL_IDLE_STATE_EVENT;
|
|
} else {
|
|
event = IdleStateEvent.ALL_IDLE_STATE_EVENT;
|
|
}
|
|
channelIdle(ctx, event);
|
|
} catch (Throwable t) {
|
|
ctx.fireExceptionCaught(t);
|
|
}
|
|
} else {
|
|
// Either read or write occurred before the timeout - set a new
|
|
// timeout with shorter delay.
|
|
allIdleTimeout = ctx.executor().schedule(this, nextDelay, TimeUnit.MILLISECONDS);
|
|
}
|
|
}
|
|
}
|
|
}
|