/*
* Created on Mar 2, 2007
*/
package ecologylab.oodss.distributed.impl;
import java.io.IOException;
import java.nio.BufferOverflowException;
import java.nio.ByteBuffer;
import java.nio.channels.SelectionKey;
import java.nio.channels.SocketChannel;
import java.nio.charset.CharsetDecoder;
import java.nio.charset.CharsetEncoder;
import java.util.HashMap;
import java.util.LinkedList;
import java.util.Map;
import java.util.Queue;
import javax.naming.OperationNotSupportedException;
import ecologylab.collections.Scope;
import ecologylab.io.ByteBufferPool;
import ecologylab.oodss.exceptions.BadClientException;
import ecologylab.oodss.exceptions.ClientOfflineException;
import ecologylab.oodss.messages.DefaultServicesTranslations;
import ecologylab.serialization.SimplTypesScope;
/**
* Handles backend, low-level communication between distributed programs, using NIO. This is the
* basis for servers for handling network communication.
*
* @author Zachary O. Toups (toupsz@cs.tamu.edu)
*/
public abstract class NIONetworking<S extends Scope> extends NIOCore
{
/**
* ByteBuffer that holds all incoming communication temporarily, immediately after it is read.
*/
private final ByteBuffer readBuffer;
/**
* Maps SocketChannels (connections) to their write Queues of ByteBuffers. Whenever a
* SocketChannel is marked for writing, and comes up for writing, the server will write the set of
* ByteBuffers to the socket.
*/
private Map<SelectionKey, Queue<ByteBuffer>> pendingWrites = new HashMap<SelectionKey, Queue<ByteBuffer>>();
protected boolean shuttingDown = false;
/**
* Space that defines mappings between xml names, and Java class names, for request messages.
*/
protected SimplTypesScope translationScope;
/** Provides a context for request processing. */
protected S objectRegistry;
protected int connectionCount = 0;
protected ByteBufferPool byteBufferPool;
protected CharsetDecoder decoder = CHARSET.newDecoder();
protected CharsetEncoder encoder = CHARSET.newEncoder();
/**
* Creates a Services Server Base. Sets internal variables, but does not bind the port. Port
* binding is to be handled by sublcasses.
*
* @param portNumber
* the port number to use for communicating.
* @param translationScope
* the TranslationSpace to use for incoming messages; if this is null, uses
* DefaultServicesTranslations instead.
* @param objectRegistry
* Provides a context for request processing; if this is null, creates a new
* ObjectRegistry.
* @throws IOException
* if an I/O error occurs while trying to open a Selector from the system.
*/
protected NIONetworking(String networkIdentifier, int portNumber,
SimplTypesScope translationScope, S objectRegistry, int maxMessageSizeChars)
throws IOException
{
super(networkIdentifier, portNumber);
if (translationScope == null)
translationScope = DefaultServicesTranslations.get();
this.translationScope = translationScope;
this.objectRegistry = objectRegistry;
readBuffer = ByteBuffer.allocateDirect((int) Math.ceil(maxMessageSizeChars
* encoder.maxBytesPerChar()));
this.byteBufferPool = new ByteBufferPool(10, 10, (int) Math.ceil(maxMessageSizeChars
* encoder.maxBytesPerChar()));
}
/**
* @see ecologylab.oodss.distributed.impl.NIOCore#readReady(java.nio.channels.SelectionKey)
*/
@Override
protected void readReady(SelectionKey key) throws ClientOfflineException, BadClientException
{
readKey(key);
}
/**
* @see ecologylab.oodss.distributed.impl.NIOCore#writeReady(java.nio.channels.SelectionKey)
*/
@Override
protected void writeReady(SelectionKey key) throws IOException
{
writeKey(key);
}
/**
* Queue up bytes to send on a particular socket. This method is typically called by some outside
* context manager, that has produced an encoded message to send out.
*
* @param socketKey
* @param data
*/
public void enqueueBytesForWriting(SelectionKey socketKey, ByteBuffer data)
{
// queue data to write
synchronized (this.pendingWrites)
{
Queue<ByteBuffer> dataQueue = pendingWrites.get(socketKey);
if (dataQueue == null)
{
dataQueue = new LinkedList<ByteBuffer>();
pendingWrites.put(socketKey, dataQueue);
}
dataQueue.offer(data);
}
this.queueForWrite(socketKey);
selector.wakeup();
}
/**
* Reads all the data from the key into the readBuffer, then pushes that information to the action
* processor for processing.
*
* @param key
* @throws BadClientException
*/
private final void readKey(SelectionKey key) throws BadClientException, ClientOfflineException
{
SocketChannel sc = (SocketChannel) key.channel();
int bytesRead;
this.readBuffer.clear();
// read
try
{
bytesRead = sc.read(readBuffer);
}
catch (BufferOverflowException e)
{
throw new BadClientException(sc.socket().getInetAddress().getHostAddress(),
"Client overflowed the buffer.");
}
catch (IOException e)
{ // error trying to read; client disconnected
throw new ClientOfflineException("Client forcibly closed connection.");
}
if (bytesRead == -1)
{ // connection closed cleanly
throw new ClientOfflineException("Client closed connection cleanly.");
}
else if (bytesRead > 0)
{
readBuffer.flip();
// get the session key that was formed at accept(), and send it over as
// the sessionId
this.processReadData(key.attachment(), key, readBuffer, bytesRead);
}
}
/**
* Writes the bytes from pendingWrites that belong to key.
*
* @param key
* @throws IOException
*/
protected void writeKey(SelectionKey key) throws IOException
{
SocketChannel sc = (SocketChannel) key.channel();
synchronized (this.pendingWrites)
{
Queue<ByteBuffer> writes = pendingWrites.get(key);
while (!writes.isEmpty())
{ // write everything
ByteBuffer bytes = writes.poll();
bytes.flip();
while (bytes.remaining() > 0)
{ // the socket's buffer filled up!; should go out again next time
//debug("unable to write all data to client; will try again shortly.");
sc.write(bytes);
}
bytes = this.byteBufferPool.release(bytes);
}
}
}
/**
* Optional operation.
*
* Called when a key has been marked for accepting. This method should be implemented by servers,
* but clients should leave this blank, unless they are also acting as servers (accepting incoming
* connections).
*
* @param key
* @throws OperationNotSupportedException
*/
protected abstract void acceptKey(SelectionKey key);
/**
* Remove the argument passed in from the set of connections we know about.
*/
protected void connectionTerminated()
{
connectionCount--;
// When thread close by unexpected way (such as client just crashes),
// this method will end the service gracefully.
terminationAction();
}
/**
* This method is called whenever bytes have been read from a socket. There is no guaranty that
* the bytes will be a valid or complete message, nor is there a guaranty about what said bytes
* encode. Implementations should be prepared to handle incomplete messages, multiple messages, or
* malformed messages in this method.
*
* @param sessionToken
* the id being use for this session.
* @param sc
* the SocketChannel from which the bytes originated.
* @param bytes
* the bytes read from the SocketChannel.
* @param bytesRead
* the number of bytes in the bytes array.
* @throws BadClientException
* if the client from which the bytes were read has transmitted something inappropriate,
* such as data too large for a buffer or a possibly malicious message.
*/
protected abstract void processReadData(Object sessionToken, SelectionKey sk, ByteBuffer bytes,
int bytesRead) throws BadClientException;
/**
* This defines the actions that server needs to perform when the client ends unexpected way.
* Detail implementations will be in subclasses.
*/
protected void terminationAction()
{
}
/**
* Retrieves a ByteBuffer object from this's pool of ByteBuffers. Typically used by a
* ContextManager to store bytes that will be later enqueued to write (and thus released by that
* method).
*
* @return
*/
public ByteBuffer acquireByteBufferFromPool()
{
return this.byteBufferPool.acquire();
}
}