/**
* The MIT License
* Copyright (c) 2014-2016 Ilkka Seppälä
*
* Permission is hereby granted, free of charge, to any person obtaining a copy
* of this software and associated documentation files (the "Software"), to deal
* in the Software without restriction, including without limitation the rights
* to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
* copies of the Software, and to permit persons to whom the Software is
* furnished to do so, subject to the following conditions:
*
* The above copyright notice and this permission notice shall be included in
* all copies or substantial portions of the Software.
*
* THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
* IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
* FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
* AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
* LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
* OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN
* THE SOFTWARE.
*/
package com.iluwatar.reactor.framework;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
import java.io.IOException;
import java.nio.channels.SelectionKey;
import java.nio.channels.Selector;
import java.nio.channels.ServerSocketChannel;
import java.nio.channels.SocketChannel;
import java.util.Iterator;
import java.util.Queue;
import java.util.Set;
import java.util.concurrent.ConcurrentLinkedQueue;
import java.util.concurrent.ExecutorService;
import java.util.concurrent.Executors;
import java.util.concurrent.TimeUnit;
/**
* This class acts as Synchronous Event De-multiplexer and Initiation Dispatcher of Reactor pattern. Multiple handles
* i.e. {@link AbstractNioChannel}s can be registered to the reactor and it blocks for events from all these handles.
* Whenever an event occurs on any of the registered handles, it synchronously de-multiplexes the event which can be any
* of read, write or accept, and dispatches the event to the appropriate {@link ChannelHandler} using the
* {@link Dispatcher}.
*
* <p>
* Implementation: A NIO reactor runs in its own thread when it is started using {@link #start()} method.
* {@link NioReactor} uses {@link Selector} for realizing Synchronous Event De-multiplexing.
*
* <p>
* NOTE: This is one of the ways to implement NIO reactor and it does not take care of all possible edge cases which are
* required in a real application. This implementation is meant to demonstrate the fundamental concepts that lie behind
* Reactor pattern.
*/
public class NioReactor {
private static final Logger LOGGER = LoggerFactory.getLogger(NioReactor.class);
private final Selector selector;
private final Dispatcher dispatcher;
/**
* All the work of altering the SelectionKey operations and Selector operations are performed in the context of main
* event loop of reactor. So when any channel needs to change its readability or writability, a new command is added
* in the command queue and then the event loop picks up the command and executes it in next iteration.
*/
private final Queue<Runnable> pendingCommands = new ConcurrentLinkedQueue<>();
private final ExecutorService reactorMain = Executors.newSingleThreadExecutor();
/**
* Creates a reactor which will use provided {@code dispatcher} to dispatch events. The application can provide
* various implementations of dispatcher which suits its needs.
*
* @param dispatcher
* a non-null dispatcher used to dispatch events on registered channels.
* @throws IOException
* if any I/O error occurs.
*/
public NioReactor(Dispatcher dispatcher) throws IOException {
this.dispatcher = dispatcher;
this.selector = Selector.open();
}
/**
* Starts the reactor event loop in a new thread.
*
* @throws IOException
* if any I/O error occurs.
*/
public void start() throws IOException {
reactorMain.execute(() -> {
try {
LOGGER.info("Reactor started, waiting for events...");
eventLoop();
} catch (IOException e) {
e.printStackTrace();
}
});
}
/**
* Stops the reactor and related resources such as dispatcher.
*
* @throws InterruptedException
* if interrupted while stopping the reactor.
* @throws IOException
* if any I/O error occurs.
*/
public void stop() throws InterruptedException, IOException {
reactorMain.shutdownNow();
selector.wakeup();
reactorMain.awaitTermination(4, TimeUnit.SECONDS);
selector.close();
}
/**
* Registers a new channel (handle) with this reactor. Reactor will start waiting for events on this channel and
* notify of any events. While registering the channel the reactor uses {@link AbstractNioChannel#getInterestedOps()}
* to know about the interested operation of this channel.
*
* @param channel
* a new channel on which reactor will wait for events. The channel must be bound prior to being registered.
* @return this
* @throws IOException
* if any I/O error occurs.
*/
public NioReactor registerChannel(AbstractNioChannel channel) throws IOException {
SelectionKey key = channel.getJavaChannel().register(selector, channel.getInterestedOps());
key.attach(channel);
channel.setReactor(this);
return this;
}
private void eventLoop() throws IOException {
while (true) {
// honor interrupt request
if (Thread.interrupted()) {
break;
}
// honor any pending commands first
processPendingCommands();
/*
* Synchronous event de-multiplexing happens here, this is blocking call which returns when it is possible to
* initiate non-blocking operation on any of the registered channels.
*/
selector.select();
/*
* Represents the events that have occurred on registered handles.
*/
Set<SelectionKey> keys = selector.selectedKeys();
Iterator<SelectionKey> iterator = keys.iterator();
while (iterator.hasNext()) {
SelectionKey key = iterator.next();
if (!key.isValid()) {
iterator.remove();
continue;
}
processKey(key);
}
keys.clear();
}
}
private void processPendingCommands() {
Iterator<Runnable> iterator = pendingCommands.iterator();
while (iterator.hasNext()) {
Runnable command = iterator.next();
command.run();
iterator.remove();
}
}
/*
* Initiation dispatcher logic, it checks the type of event and notifier application specific event handler to handle
* the event.
*/
private void processKey(SelectionKey key) throws IOException {
if (key.isAcceptable()) {
onChannelAcceptable(key);
} else if (key.isReadable()) {
onChannelReadable(key);
} else if (key.isWritable()) {
onChannelWritable(key);
}
}
private static void onChannelWritable(SelectionKey key) throws IOException {
AbstractNioChannel channel = (AbstractNioChannel) key.attachment();
channel.flush(key);
}
private void onChannelReadable(SelectionKey key) {
try {
// reads the incoming data in context of reactor main loop. Can this be improved?
Object readObject = ((AbstractNioChannel) key.attachment()).read(key);
dispatchReadEvent(key, readObject);
} catch (IOException e) {
try {
key.channel().close();
} catch (IOException e1) {
e1.printStackTrace();
}
}
}
/*
* Uses the application provided dispatcher to dispatch events to application handler.
*/
private void dispatchReadEvent(SelectionKey key, Object readObject) {
dispatcher.onChannelReadEvent((AbstractNioChannel) key.attachment(), readObject, key);
}
private void onChannelAcceptable(SelectionKey key) throws IOException {
ServerSocketChannel serverSocketChannel = (ServerSocketChannel) key.channel();
SocketChannel socketChannel = serverSocketChannel.accept();
socketChannel.configureBlocking(false);
SelectionKey readKey = socketChannel.register(selector, SelectionKey.OP_READ);
readKey.attach(key.attachment());
}
/**
* Queues the change of operations request of a channel, which will change the interested operations of the channel
* sometime in future.
* <p>
* This is a non-blocking method and does not guarantee that the operations have changed when this method returns.
*
* @param key
* the key for which operations have to be changed.
* @param interestedOps
* the new interest operations.
*/
public void changeOps(SelectionKey key, int interestedOps) {
pendingCommands.add(new ChangeKeyOpsCommand(key, interestedOps));
selector.wakeup();
}
/**
* A command that changes the interested operations of the key provided.
*/
class ChangeKeyOpsCommand implements Runnable {
private SelectionKey key;
private int interestedOps;
public ChangeKeyOpsCommand(SelectionKey key, int interestedOps) {
this.key = key;
this.interestedOps = interestedOps;
}
public void run() {
key.interestOps(interestedOps);
}
@Override
public String toString() {
return "Change of ops to: " + interestedOps;
}
}
}