/*
* Licensed to the Apache Software Foundation (ASF) under one
* or more contributor license agreements. See the NOTICE file
* distributed with this work for additional information
* regarding copyright ownership. The ASF 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 com.github.zangxiaoqiang.io.nio;
import java.io.IOException;
import java.nio.ByteBuffer;
import java.nio.channels.ClosedChannelException;
import java.nio.channels.SelectableChannel;
import java.nio.channels.SelectionKey;
import java.nio.channels.Selector;
import java.util.Iterator;
import java.util.Queue;
import java.util.concurrent.ConcurrentLinkedQueue;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
/**
* This class holds a Selector and handle all the incoming events for the sessions registered on this selector.ALl the
* events will be processed by some dedicated thread, taken from a pool. It will loop forever, untill the instance is
* stopped.
*
* @author <a href="http://mina.apache.org">Apache MINA Project</a>
*/
public class NioSelectorLoop implements SelectorLoop {
/** The logger for this class */
private static final Logger LOG = LoggerFactory.getLogger(NioSelectorLoop.class);
private static final boolean IS_DEBUG = LOG.isDebugEnabled();
/** the selector managed by this class */
private Selector selector;
/** Read buffer for all the incoming bytes (default to 64Kb) */
private final ByteBuffer readBuffer = ByteBuffer.allocateDirect(64 * 1024);
/** The queue containing the channels to register on the selector */
private final Queue<Registration> registrationQueue = new ConcurrentLinkedQueue<Registration>();
/**
* Creates an instance of the SelectorLoop.
*
* @param prefix
* @param index
*/
public NioSelectorLoop(final String prefix) {
this(prefix, -1);
}
/**
* Creates an instance of the SelectorLoop.
*
* @param prefix
* @param index
*/
public NioSelectorLoop(final String prefix, final int index) {
String workerName = "SelectorWorker " + prefix;
if (index >= 0) {
workerName += "-" + index;
}
SelectorWorker worker = new SelectorWorker(workerName);
try {
if (IS_DEBUG) {
LOG.debug("open a selector");
}
selector = Selector.open();
} catch (final IOException ioe) {
LOG.error("Impossible to open a new NIO selector, O/S is out of file descriptor ?");
throw new IllegalStateException("Impossible to open a new NIO selector, O/S is out of file descriptor ?",
ioe);
}
if (IS_DEBUG) {
LOG.debug("starting worker thread");
}
worker.start();
}
/**
* {@inheritDoc}
*/
public void register(boolean accept, boolean connect, boolean read, boolean write, SelectorListener listener,
SelectableChannel channel, RegistrationCallback callback) {
if (IS_DEBUG) {
LOG.debug("registering : {} for accept : {}, connect: {}, read : {}, write : {}, channel : {}",
new Object[] { listener, accept, connect, read, write, channel });
}
int ops = 0;
if (accept) {
ops |= SelectionKey.OP_ACCEPT;
}
if (connect) {
ops |= SelectionKey.OP_CONNECT;
}
if (read) {
ops |= SelectionKey.OP_READ;
}
if (write) {
ops |= SelectionKey.OP_WRITE;
}
// TODO : if it's the same selector/worker, we don't need to do that we could directly enqueue
registrationQueue.add(new Registration(ops, channel, listener, callback));
// Now, wakeup the selector in order to let it update the selectionKey status
wakeup();
}
/**
* {@inheritDoc}
*/
public void modifyRegistration(boolean accept, boolean read, boolean write, final SelectorListener listener,
SelectableChannel channel, boolean wakeup) {
if (IS_DEBUG) {
LOG.debug("modifying registration : {} for accept : {}, read : {}, write : {}, channel : {}", new Object[] {
listener, accept, read, write, channel });
}
final SelectionKey key = channel.keyFor(selector);
if (key == null) {
LOG.error("Trying to modify the registration of a not registered channel");
return;
}
int ops = 0;
if (accept) {
ops |= SelectionKey.OP_ACCEPT;
}
if (read) {
ops |= SelectionKey.OP_READ;
}
if (write) {
ops |= SelectionKey.OP_WRITE;
}
key.interestOps(ops);
// we need to wakeup for the registration to be modified (TODO : not needed if we are in the worker thread)
if (wakeup) {
wakeup();
}
}
/**
* {@inheritDoc}
*/
public void unregister(final SelectorListener listener, final SelectableChannel channel) {
if (IS_DEBUG) {
LOG.debug("unregistering : {}", listener);
}
final SelectionKey key = channel.keyFor(selector);
if (key == null) {
LOG.error("Trying to modify the registration of a not registered channel");
return;
}
key.cancel();
key.attach(null);
if (IS_DEBUG) {
LOG.debug("unregistering : {} done !", listener);
}
}
/**
* The worker processing incoming session creation, session destruction requests, session write and reads. It will
* also bind new servers.
*/
private class SelectorWorker extends Thread {
public SelectorWorker(String name) {
super(name);
setDaemon(true);
}
@Override
public void run() {
for (;;) {
try {
if (IS_DEBUG) {
LOG.debug("selecting...");
}
final int readyCount = selector.select();
if (IS_DEBUG) {
LOG.debug("... done selecting : {} events", readyCount);
}
if (readyCount > 0) {
final Iterator<SelectionKey> it = selector.selectedKeys().iterator();
while (it.hasNext()) {
final SelectionKey key = it.next();
final SelectorListener listener = (SelectorListener) key.attachment();
int ops = key.readyOps();
boolean isAcceptable = (ops & SelectionKey.OP_ACCEPT) == SelectionKey.OP_ACCEPT;
boolean isConnectable = (ops & SelectionKey.OP_CONNECT) == SelectionKey.OP_CONNECT;
boolean isReadable = (ops & SelectionKey.OP_READ) == SelectionKey.OP_READ;
boolean isWritable = (ops & SelectionKey.OP_WRITE) == SelectionKey.OP_WRITE;
readBuffer.clear();
listener.ready(isAcceptable, isConnectable, isReadable, isReadable ? readBuffer : null,
isWritable, key);
// if you don't remove the event of the set, the selector will present you this event again
// and again
if (IS_DEBUG) {
LOG.debug("remove");
}
it.remove();
}
}
// new registration
while (!registrationQueue.isEmpty()) {
final Registration reg = registrationQueue.poll();
try {
SelectionKey selectionKey = reg.channel.register(selector, reg.ops, reg.listener);
if (reg.getCallback() != null) {
reg.getCallback().done(selectionKey);
}
} catch (final ClosedChannelException ex) {
// dead session..
LOG.error("socket is already dead", ex);
}
}
} catch (final Exception e) {
LOG.error("Unexpected exception : ", e);
}
}
}
}
public void wakeup() {
selector.wakeup();
}
private static class Registration {
public Registration(int ops, SelectableChannel channel, SelectorListener listener, RegistrationCallback callback) {
this.ops = ops;
this.channel = channel;
this.listener = listener;
this.callback = callback;
}
private final int ops;
private final SelectableChannel channel;
private final SelectorListener listener;
private final RegistrationCallback callback;
public RegistrationCallback getCallback() {
return callback;
}
@Override
public String toString() {
StringBuilder sb = new StringBuilder();
sb.append("Registration : [");
boolean hasOp = false;
if ((ops & SelectionKey.OP_READ) == SelectionKey.OP_READ) {
sb.append("OP_READ");
hasOp = true;
}
if ((ops & SelectionKey.OP_WRITE) == SelectionKey.OP_WRITE) {
if (hasOp) {
sb.append("|");
}
sb.append("OP_WRITE");
hasOp = true;
}
if ((ops & SelectionKey.OP_ACCEPT) == SelectionKey.OP_ACCEPT) {
if (hasOp) {
sb.append("|");
}
sb.append("OP_ACCEPT");
hasOp = true;
}
if ((ops & SelectionKey.OP_CONNECT) == SelectionKey.OP_CONNECT) {
if (hasOp) {
sb.append("|");
}
sb.append("OP_CONNECT");
hasOp = true;
}
if (channel != null) {
sb.append(", ").append(channel);
}
return sb.toString();
}
}
}