/**
*
*/
package ecologylab.oodss.distributed.impl;
import java.io.IOException;
import java.nio.channels.CancelledKeyException;
import java.nio.channels.SelectionKey;
import java.nio.channels.Selector;
import java.nio.channels.SocketChannel;
import java.util.Iterator;
import java.util.Queue;
import java.util.concurrent.ConcurrentLinkedQueue;
import ecologylab.generic.Debug;
import ecologylab.generic.ResourcePool;
import ecologylab.generic.StartAndStoppable;
import ecologylab.oodss.distributed.common.NetworkingConstants;
import ecologylab.oodss.exceptions.BadClientException;
import ecologylab.oodss.exceptions.ClientOfflineException;
/**
* Provides core functionality for NIO-based servers or clients. This class is Runnable and
* StartAndStoppable; it's run method automatically handles interest-switching on a selector's keys,
* as well as calling appropriate abstract methods whenever interest ops are selected.
*
* Subclasses are required to configure their own selector.
*
* @author Zachary O. Toups (toupsz@cs.tamu.edu)
*
*/
public abstract class NIOCore extends Debug implements StartAndStoppable, NetworkingConstants
{
private Queue<SocketModeChangeRequest> pendingSelectionOpChanges = new ConcurrentLinkedQueue<SocketModeChangeRequest>();
protected Selector selector;
private String networkingIdentifier = "NIOCore";
private volatile boolean running;
private Thread thread;
protected int portNumber;
private SocketModeChangeRequestPool mReqPool = new SocketModeChangeRequestPool(
20, 10);
/**
* Instantiates a new NIOCore object.
*
* @param networkingIdentifier
* the name to identify this object when its thread is created.
* @param portNumber
* the port number that this object will use for network communications.
* @throws IOException
* if an I/O error occurs while trying to open a Selector from the system.
*/
protected NIOCore(String networkingIdentifier, int portNumber) throws IOException
{
this.networkingIdentifier = networkingIdentifier;
this.portNumber = portNumber;
}
/**
* THIS METHOD SHOULD NOT BE CALLED DIRECTLY!
*
* Proper use of this method is through the start / stop methods.
*
* Main run method. Performs a loop of changing the mode (read/write) for each socket, if
* requested, then checks for and performs appropriate I/O for each socket that is ready. Ends
* when running is set to false (through the stop method).
*
* @see java.lang.Runnable#run()
*/
@Override
public final void run()
{
while (running)
{
// update pending selection operation changes
synchronized (this.pendingSelectionOpChanges)
{
for (SocketModeChangeRequest changeReq : pendingSelectionOpChanges)
{
if (changeReq.key.channel().isRegistered())
{
/*
* Perform any changes to the interest ops on the keys, before selecting.
*/
switch (changeReq.type)
{
case CHANGEOPS:
try
{
changeReq.key.interestOps(changeReq.ops);
}
catch (CancelledKeyException e)
{
debug("tried to change ops after key was cancelled.");
}
catch (IllegalArgumentException e1)
{
debug("illegal argument for interestOps: " + changeReq.ops);
}
break;
case INVALIDATE_PERMANENTLY:
debug(">>>>>>>>>>>>>>>> invalidating permanently: " + changeReq.key.attachment());
invalidateKey(changeReq.key, true);
break;
case INVALIDATE_TEMPORARILY:
debug(">>>>>>>>>>>>>>>> invalidating temporarily: " + changeReq.key.attachment());
invalidateKey(changeReq.key, false);
break;
}
}
// release the SocketModeChangeRequest when done
changeReq = this.mReqPool.release(changeReq);
}
this.pendingSelectionOpChanges.clear();
}
// check selection operations
try
{
if (selector.select() > 0)
{
/*
* get an iterator of the keys that have something to do we have to do it this way,
* because we have to be able to call remove() which will not work in a foreach loop
*/
Iterator<SelectionKey> selectedKeyIter = selector.selectedKeys().iterator();
while (selectedKeyIter.hasNext())
{
/*
* get the key corresponding to the event and process it appropriately, then remove it
*/
SelectionKey key = selectedKeyIter.next();
selectedKeyIter.remove();
if (!key.isValid())
{
debug("invalid key");
setPendingInvalidate(key, false);
}
else if (key.isReadable())
{
/*
* incoming readable, valid key; have to double-check validity here, because accept
* key may have rejected an incoming connection
*/
if (key.channel().isOpen() && key.isValid())
{
try
{
readReady(key);
readFinished(key);
}
catch (ClientOfflineException e)
{
warning(e.getMessage());
setPendingInvalidate(key, false);
}
catch (BadClientException e)
{
// close down this evil connection!
error(e.getMessage());
this.removeBadConnections(key);
}
catch (java.lang.IllegalArgumentException e)
{
warning(e.getMessage());
}
}
else
{
debug("Channel closed on " + key.attachment() + ", removing.");
invalidateKey(key, false);
}
}
else if (key.isWritable())
{
try
{
writeReady(key);
writeFinished(key);
}
catch (IOException e)
{
debug("IO error when attempting to write to socket; stack trace follows.");
e.printStackTrace();
}
}
else if (key.isAcceptable())
{ // incoming connection; accept
this.acceptReady(key);
this.acceptFinished(key);
}
else if (key.isConnectable())
{
this.connectReady(key);
this.connectFinished(key);
}
}
}
}
catch (IOException e)
{
this.stop();
debug("attempted to access selector after it was closed! shutting down");
e.printStackTrace();
}
// remove any that were idle for too long
this.checkAndDropIdleKeys();
}
this.close();
}
public void setPriority(int priority)
{
Thread thread = this.thread;
if (thread != null)
{
thread.setPriority(priority);
}
}
/**
* @param key
*/
protected abstract void acceptReady(SelectionKey key);
/**
* @param key
*/
protected abstract void connectReady(SelectionKey key);
/**
* @param key
*/
protected abstract void readFinished(SelectionKey key);
/**
* @param key
*/
protected abstract void readReady(SelectionKey key) throws ClientOfflineException,
BadClientException;
/**
* Queues a request to change key's interest operations back to READ.
*
* This method is automatically called after acceptReady(SelectionKey) in the main operating loop.
*
* @param key
*/
public abstract void acceptFinished(SelectionKey key);
/**
* Queues a request to change key's interest operations back to READ.
*
* This method is automatically called after connectReady(SelectionKey) in the main operating
* loop.
*
* @param key
*/
public void connectFinished(SelectionKey key)
{
this.queueForRead(key);
selector.wakeup();
}
/**
* Queues a request to change key's interest operations back to READ.
*
* This method is automatically called after writeReady(SelectionKey) in the main operating loop.
*
* Perform any actions necessary after all data has been written from the outgoing queue to the
* client for this key. This is a hook method so that subclasses can provide specific
* functionality (such as, for example, invalidating the connection once the data has been sent.
*
* @param key
* - the SelectionKey that is finished writing.
*/
protected void writeFinished(SelectionKey key)
{
this.queueForRead(key);
selector.wakeup();
}
protected abstract void removeBadConnections(SelectionKey key);
/**
* Sets up a pending invalidate command for the given input.
*
* @param key
* the key to invalidate
* @param forcePermanent
* ignore any settings for the client and invalidate permanently no matter what
*/
public void setPendingInvalidate(SelectionKey key, boolean forcePermanent)
{
// allow subclass processing of the key being invalidated, and find out if
// it should be permanent
boolean permanent = this.handleInvalidate(key, forcePermanent);
SocketModeChangeRequest req = this.mReqPool.acquire();
req.key = key;
req.type = ((forcePermanent ? true : permanent) ? SocketModeChangeRequestType.INVALIDATE_PERMANENTLY
: SocketModeChangeRequestType.INVALIDATE_TEMPORARILY);
synchronized (pendingSelectionOpChanges)
{
this.pendingSelectionOpChanges.offer(req);
}
selector.wakeup();
}
/**
* Checks the key to see what type of invalidation it should be (permanent, or temporary) and
* handles any housecleaning associated with invalidating that key.
*
* @param key
* - the key
* @param forcePermanent
* @return true if the key should be invalidated permanently, or false otherwise
*/
protected abstract boolean handleInvalidate(SelectionKey key, boolean forcePermanent);
/**
* Shut down the connection associated with this SelectionKey. Subclasses should override to do
* your own housekeeping, then call super.invalidateKey(SelectionKey) to utilize the functionality
* here.
*
* @param chan
* The SocketChannel that needs to be shut down.
*/
protected void invalidateKey(SocketChannel chan)
{
try
{
chan.close();
}
catch (IOException e)
{
debug(e.getMessage());
}
catch (NullPointerException e)
{
debug(e.getMessage());
}
if (chan.keyFor(selector) != null)
{ /*
* it's possible that they key was somehow disposed of already, perhaps it was already
* invalidated once
*/
chan.keyFor(selector).cancel();
}
}
/**
* @see ecologylab.oodss.distributed.impl.NIONetworking#invalidateKey(java.nio.channels.SocketChannel,
* boolean)
*/
protected abstract void invalidateKey(SelectionKey key, boolean permanent);
@Override
public void start()
{
// start the server running
running = true;
if (thread == null)
{
thread = new Thread(this, networkingIdentifier + " running on port " + portNumber);
synchronized (thread)
{
thread.start();
}
}
}
protected void openSelector() throws IOException
{
selector = Selector.open();
}
@Override
public synchronized void stop()
{
running = false;
try
{
this.selector.wakeup();
}
catch (Exception e)
{
e.printStackTrace();
}
this.close();
if (thread != null)
{
synchronized (thread)
{ // we cannot re-use the Thread object.
thread = null;
}
}
}
protected void close()
{
}
/**
* Check for timeout on all allocated keys; deallocate those that are hanging around, but no
* longer in use.
*/
protected abstract void checkAndDropIdleKeys();
/**
* @param key
*/
protected abstract void writeReady(SelectionKey key) throws IOException;
protected void queueForAccept(SelectionKey key)
{
SocketModeChangeRequest req = this.mReqPool.acquire();
req.key = key;
req.type = SocketModeChangeRequestType.CHANGEOPS;
req.ops = SelectionKey.OP_ACCEPT;
synchronized (this.pendingSelectionOpChanges)
{
// queue the socket channel for writing
this.pendingSelectionOpChanges.offer(req);
}
}
protected void queueForConnect(SelectionKey key)
{
SocketModeChangeRequest req = this.mReqPool.acquire();
req.key = key;
req.type = SocketModeChangeRequestType.CHANGEOPS;
req.ops = SelectionKey.OP_CONNECT;
synchronized (this.pendingSelectionOpChanges)
{
// queue the socket channel for writing
this.pendingSelectionOpChanges.offer(req);
}
}
protected void queueForRead(SelectionKey key)
{
SocketModeChangeRequest req = this.mReqPool.acquire();
req.key = key;
req.type = SocketModeChangeRequestType.CHANGEOPS;
req.ops = SelectionKey.OP_READ;
synchronized (this.pendingSelectionOpChanges)
{
// queue the socket channel for writing
this.pendingSelectionOpChanges.offer(req);
}
}
protected void queueForWrite(SelectionKey key)
{
SocketModeChangeRequest req = this.mReqPool.acquire();
req.key = key;
req.type = SocketModeChangeRequestType.CHANGEOPS;
req.ops = SelectionKey.OP_WRITE;
synchronized (this.pendingSelectionOpChanges)
{
// queue the socket channel for writing
this.pendingSelectionOpChanges.offer(req);
}
}
/**
* @return the port number the server is listening on.
*/
public int getPortNumber()
{
return portNumber;
}
protected enum SocketModeChangeRequestType
{
/**
* Indicates that the socket mode should not actually be changed; only used for fresh
* SocketModeChangeRequests that have not yet been specified.
*/
NONE,
/**
* Indicates that the socket mode should change it's interest ops to those specified by the
* SocketModeChangeRequest.
*/
CHANGEOPS,
/**
* Indicates that the socket should be permanently invalidated and it's matching client manager
* should be destroyed. This should only happen when clients purposefully disconnect or when
* they have been banned. Some special servers (such as HTTP servers) may also invalidate
* permanently when the socket disconnects.
*/
INVALIDATE_PERMANENTLY,
/**
* Indicates that the socket should be temporarily disconnected. This results from an unexpected
* disconnect by the client. The result is that the matching client manager should be retained
* for some period of time, so that the client can reconnect if desired.
*/
INVALIDATE_TEMPORARILY
}
/**
* A signaling object for modifying interest ops and socket invalidation in a thread-safe way.
*
* @author James Greenfield
*/
class SocketModeChangeRequest
{
public SelectionKey key;
public SocketModeChangeRequestType type;
public int ops;
public SocketModeChangeRequest(SelectionKey key, SocketModeChangeRequestType type, int ops)
{
this.key = key;
this.type = type;
this.ops = ops;
}
}
/**
* A resource pool that handles socket mode change requests to prevent unnecessary instantiations.
*
* @author Zachary O. Toups (toupsz@cs.tamu.edu)
*/
class SocketModeChangeRequestPool extends ResourcePool<SocketModeChangeRequest>
{
/**
* @param initialPoolSize
* @param minimumPoolSize
*/
public SocketModeChangeRequestPool(int initialPoolSize, int minimumPoolSize)
{
super(initialPoolSize, minimumPoolSize);
}
/**
* @see ecologylab.generic.ResourcePool#clean(java.lang.Object)
*/
@Override
protected void clean(SocketModeChangeRequest objectToClean)
{
objectToClean.key = null;
objectToClean.ops = 0;
objectToClean.type = SocketModeChangeRequestType.NONE;
}
/**
* @see ecologylab.generic.ResourcePool#generateNewResource()
*/
@Override
protected SocketModeChangeRequest generateNewResource()
{
return new SocketModeChangeRequest(null, SocketModeChangeRequestType.NONE, 0);
}
}
}