/*
* Copyright 2014 the original author or authors.
*
* Licensed 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 org.apache.nifi.processors.gettcp;
import java.io.IOException;
import java.net.InetSocketAddress;
import java.nio.ByteBuffer;
import java.nio.channels.NetworkChannel;
import java.nio.channels.SelectionKey;
import java.nio.channels.Selector;
import java.nio.channels.SocketChannel;
import java.util.HashSet;
import java.util.Iterator;
import java.util.Set;
import java.util.concurrent.ExecutorService;
import java.util.concurrent.Executors;
import java.util.concurrent.atomic.AtomicBoolean;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
/**
* Base class to implement async TCP Client/Server components
*
*/
abstract class AbstractSocketHandler {
final Logger logger = LoggerFactory.getLogger(this.getClass());
private final ByteBuffer readingBuffer;
private final Runnable listenerTask;
private volatile ExecutorService listenerTaskExecutor;
final InetSocketAddress address;
volatile NetworkChannel rootChannel;
volatile Selector selector;
private final AtomicBoolean isRunning;
protected final byte endOfMessageByte;
/**
* This constructor configures the address to bind to, the size of the buffer to use for reading, and
* the byte pattern to use for demarcating the end of a message.
*
* @param address the socket address
* @param readingBufferSize the buffer size
* @param endOfMessageByte the byte indicating EOM
*/
public AbstractSocketHandler(InetSocketAddress address, int readingBufferSize, byte endOfMessageByte) {
this.address = address;
this.listenerTask = new ListenerTask();
this.readingBuffer = ByteBuffer.allocate(readingBufferSize);
this.isRunning = new AtomicBoolean();
this.endOfMessageByte = endOfMessageByte;
}
/**
* Once the handler is constructed this should be called to start the handler. Although
* this method is safe to be called by multiple threads, it should only be called once.
*
* @throws IllegalStateException if it fails to start listening on the port that is configured
*
*/
public void start() {
if (this.isRunning.compareAndSet(false, true)) {
try {
if (this.selector == null || !this.selector.isOpen()) {
this.selector = Selector.open();
InetSocketAddress connectedAddress = this.connect();
this.listenerTaskExecutor = Executors.newCachedThreadPool();
this.listenerTaskExecutor.execute(this.listenerTask);
if (logger.isDebugEnabled()) {
logger.debug("Started listener for " + AbstractSocketHandler.this.getClass().getSimpleName());
}
if (logger.isInfoEnabled()) {
logger.info("Successfully bound to " + connectedAddress);
}
}
} catch (Exception e) {
this.stop();
throw new IllegalStateException("Failed to start " + this.getClass().getName(), e);
}
}
}
/**
* This should be called to stop the handler from listening on the socket. Although it is recommended
* that this is called once, by a single thread, this method does protect itself from being called by more
* than one thread and more than one time.
*
*/
public void stop() {
if (this.isRunning.compareAndSet(true, false)) {
try {
if (this.selector != null && this.selector.isOpen()) { // since stop must be idempotent, we need to check if selector is open to avoid ClosedSelectorException
Set<SelectionKey> selectionKeys = new HashSet<>(this.selector.keys());
for (SelectionKey key : selectionKeys) {
key.cancel();
try {
key.channel().close();
} catch (IOException e) {
logger.warn("Failure while closing channel", e);
}
}
try {
this.selector.close();
} catch (Exception e) {
logger.warn("Failure while closinig selector", e);
}
logger.info(this.getClass().getSimpleName() + " is stopped listening on " + address);
}
} finally {
if (this.listenerTaskExecutor != null) {
this.listenerTaskExecutor.shutdown();
}
}
}
}
/**
* Checks if this component is running.
*/
public boolean isRunning() {
return this.isRunning.get();
}
/**
* This must be overridden by an implementing class and should establish the socket connection.
*
* @throws Exception if an exception occurs
*/
abstract InetSocketAddress connect() throws Exception;
/**
* Will process the data received from the channel.
*
* @param selectionKey key for the channel the data came from
* @param buffer buffer of received data
* @throws IOException if there is a problem processing the data
*/
abstract void processData(SelectionKey selectionKey, ByteBuffer buffer) throws IOException;
/**
* This does not perform any work at this time as all current implementations of this class
* provide the client side of the connection and thus do not accept connections.
*
* @param selectionKey the selection key
* @throws IOException if there is a problem
*/
void doAccept(SelectionKey selectionKey) throws IOException {
// noop
}
/**
* Main listener task which will process delegate {@link SelectionKey}
* selected from the {@link Selector} to the appropriate processing method
* (e.g., accept, read, write etc.)
*/
private class ListenerTask implements Runnable {
@Override
public void run() {
try {
while (AbstractSocketHandler.this.rootChannel != null && AbstractSocketHandler.this.rootChannel.isOpen() && AbstractSocketHandler.this.selector.isOpen()) {
if (AbstractSocketHandler.this.selector.isOpen() && AbstractSocketHandler.this.selector.select(10) > 0) {
Iterator<SelectionKey> keys = AbstractSocketHandler.this.selector.selectedKeys().iterator();
while (keys.hasNext()) {
SelectionKey selectionKey = keys.next();
keys.remove();
if (selectionKey.isValid()) {
if (selectionKey.isAcceptable()) {
this.accept(selectionKey);
} else if (selectionKey.isReadable()) {
this.read(selectionKey);
} else if (selectionKey.isConnectable()) {
this.connect(selectionKey);
}
}
}
}
}
} catch (Exception e) {
logger.error("Exception in socket listener loop", e);
}
logger.debug("Exited Listener loop.");
AbstractSocketHandler.this.stop();
}
/**
* Accept the selectable channel
*
* @throws IOException in the event that something goes wrong accepting it
*/
private void accept(SelectionKey selectionKey) throws IOException {
AbstractSocketHandler.this.doAccept(selectionKey);
}
/**
* This will connect the channel; if it is in a pending state then this will finish
* establishing the connection. Finally the socket handler is registered with this
* channel.
*
* @throws IOException if anything goes wrong during the connection establishment
* or registering of the handler
*/
private void connect(SelectionKey selectionKey) throws IOException {
SocketChannel clientChannel = (SocketChannel) selectionKey.channel();
if (clientChannel.isConnectionPending()) {
clientChannel.finishConnect();
}
clientChannel.register(AbstractSocketHandler.this.selector, SelectionKey.OP_READ);
}
/**
* The main read loop which reads packets from the channel and sends
* them to implementations of
* {@link AbstractSocketHandler#processData(SelectionKey, ByteBuffer)}.
* So if a given implementation is a Server it is probably going to
* broadcast received message to all connected sockets (e.g., chat
* server). If such implementation is the Client, then it is most likely
* the end of the road where message is processed.
*/
private void read(SelectionKey selectionKey) throws IOException {
SocketChannel socketChannel = (SocketChannel) selectionKey.channel();
int count = -1;
boolean finished = false;
while (!finished && (count = socketChannel.read(AbstractSocketHandler.this.readingBuffer)) > 0){
byte lastByte = AbstractSocketHandler.this.readingBuffer.get(AbstractSocketHandler.this.readingBuffer.position() - 1);
if (AbstractSocketHandler.this.readingBuffer.remaining() == 0 || lastByte == AbstractSocketHandler.this.endOfMessageByte) {
this.processBuffer(selectionKey);
if (lastByte == AbstractSocketHandler.this.endOfMessageByte) {
finished = true;
}
}
}
if (count == -1) {
if (AbstractSocketHandler.this.readingBuffer.position() > 0) {// flush remainder, since nothing else is coming
this.processBuffer(selectionKey);
}
selectionKey.cancel();
socketChannel.close();
if (logger.isInfoEnabled()) {
logger.info("Connection closed by: " + socketChannel.socket());
}
}
}
private void processBuffer(SelectionKey selectionKey) throws IOException {
AbstractSocketHandler.this.readingBuffer.flip();
byte[] message = new byte[AbstractSocketHandler.this.readingBuffer.limit()];
AbstractSocketHandler.this.readingBuffer.get(message);
AbstractSocketHandler.this.processData(selectionKey, ByteBuffer.wrap(message));
AbstractSocketHandler.this.readingBuffer.clear();
}
}
}