/**
*
*/
package ecologylab.oodss.distributed.server;
import java.io.IOException;
import java.net.BindException;
import java.net.InetAddress;
import java.nio.ByteBuffer;
import java.nio.CharBuffer;
import java.nio.channels.SelectionKey;
import java.nio.charset.CharacterCodingException;
import java.nio.charset.Charset;
import java.nio.charset.CharsetDecoder;
import java.util.Arrays;
import java.util.Iterator;
import ecologylab.collections.Scope;
import ecologylab.generic.CharBufferPool;
import ecologylab.generic.HashMapArrayList;
import ecologylab.generic.StringBuilderPool;
import ecologylab.io.ByteBufferPool;
import ecologylab.net.NetTools;
import ecologylab.oodss.distributed.common.ServerConstants;
import ecologylab.oodss.distributed.common.SessionObjects;
import ecologylab.oodss.distributed.impl.AbstractNIOServer;
import ecologylab.oodss.distributed.impl.NIOServerIOThread;
import ecologylab.oodss.distributed.server.clientsessionmanager.BaseSessionManager;
import ecologylab.oodss.distributed.server.clientsessionmanager.ClientSessionManager;
import ecologylab.oodss.distributed.server.clientsessionmanager.SessionHandle;
import ecologylab.oodss.distributed.server.clientsessionmanager.TCPClientSessionManager;
import ecologylab.oodss.exceptions.BadClientException;
import ecologylab.serialization.SimplTypesScope;
/**
* A server that uses NIO and two threads (one for handling IO, the other for handling interfacing
* with messages).
*
* Automatically processes and responds to any client RequestMessages.
*
* Subclasses should generally override the generateContextManager hook method, so that they can use
* their own, specific ContextManager in place of the default.
*
* @author Zachary O. Toups (zach@ecologylab.net)
*/
public class DoubleThreadedNIOServer<S extends Scope> extends AbstractNIOServer<S> implements
ServerConstants
{
protected static InetAddress[] addressToAddresses(InetAddress address)
{
InetAddress[] addresses =
{ address };
return addresses;
}
public static DoubleThreadedNIOServer getInstance(int portNumber, InetAddress[] inetAddress,
SimplTypesScope requestTranslationScope, Scope applicationObjectScope,
int idleConnectionTimeout, int maxPacketSize) throws IOException, BindException
{
return new DoubleThreadedNIOServer(portNumber, inetAddress, requestTranslationScope,
applicationObjectScope, idleConnectionTimeout, maxPacketSize);
}
public static DoubleThreadedNIOServer getInstance(int portNumber, InetAddress inetAddress,
SimplTypesScope requestTranslationScope, Scope applicationObjectScope,
int idleConnectionTimeout, int maxPacketSize) throws IOException, BindException
{
InetAddress[] address =
{ inetAddress };
return getInstance(portNumber, address, requestTranslationScope, applicationObjectScope,
idleConnectionTimeout, maxPacketSize);
}
Thread t = null;
boolean running = false;
/**
* Map in which keys are sessionTokens, and values are associated ClientSessionManagers.
*/
private HashMapArrayList<Object, TCPClientSessionManager> clientSessionManagerMap = new HashMapArrayList<Object, TCPClientSessionManager>();
/**
* Map in which keys are sessionTokens, and values are associated SessionHandles
*/
private HashMapArrayList<Object, SessionHandle> clientSessionHandleMap = new HashMapArrayList<Object, SessionHandle>();
private static final Charset ENCODED_CHARSET = Charset
.forName(CHARACTER_ENCODING);
private static CharsetDecoder DECODER = ENCODED_CHARSET
.newDecoder();
protected int maxMessageSize;
/**
* CharBuffers for use with translating from bytes to chars; may need to support having many
* messages come through at once.
*/
protected CharBufferPool charBufferPool;
protected ByteBufferPool byteBufferPool;
protected StringBuilderPool stringBuilderPool;
/**
*
*/
protected DoubleThreadedNIOServer(int portNumber, InetAddress[] inetAddresses,
SimplTypesScope requestTranslationScope, S applicationObjectScope,
int idleConnectionTimeout, int maxMessageSize) throws IOException, BindException
{
super(portNumber, inetAddresses, requestTranslationScope, applicationObjectScope,
idleConnectionTimeout, maxMessageSize);
this.maxMessageSize = maxMessageSize + MAX_HTTP_HEADER_LENGTH;
applicationObjectScope.put(SessionObjects.SESSIONS_MAP, clientSessionHandleMap);
instantiateBufferPools(this.maxMessageSize);
}
/**
* @param maxMessageSize
*/
protected void instantiateBufferPools(int maxMessageSize)
{
instantiateBufferPools(4, 4, maxMessageSize);
}
protected void instantiateBufferPools(int poolSize, int minimumCapicity, int maxMessageSize)
{
this.charBufferPool = new CharBufferPool(poolSize, minimumCapicity, maxMessageSize);
this.byteBufferPool = new ByteBufferPool(poolSize, minimumCapicity, maxMessageSize);
this.stringBuilderPool = new StringBuilderPool(poolSize, minimumCapicity, maxMessageSize);
}
/**
*
*/
protected DoubleThreadedNIOServer(int portNumber, InetAddress inetAddress,
SimplTypesScope requestTranslationScope, S applicationObjectScope,
int idleConnectionTimeout, int maxPacketSize) throws IOException, BindException
{
this(portNumber, NetTools.wrapSingleAddress(inetAddress), requestTranslationScope,
applicationObjectScope, idleConnectionTimeout, maxPacketSize);
}
/**
* Assumes that the server should be running on the local host (including external interfaces)
* with default sizes for everything.
*
* @param portNumber
* - the port number the server will run on.
* @param requestTranslationScope
* - the scope of translation for incoming requests.
* @param applicationObjectScope
* - the application object scope, containing application state objects that messages
* will access and manipulate.
* @throws IOException
* @throws BindException
*/
protected DoubleThreadedNIOServer(int portNumber, SimplTypesScope requestTranslationScope,
S applicationObjectScope) throws BindException, IOException
{
this(portNumber, NetTools.getAllInetAddressesForLocalhost(), requestTranslationScope,
applicationObjectScope, DEFAULT_IDLE_TIMEOUT, DEFAULT_MAX_MESSAGE_LENGTH_CHARS);
}
@Override
public void processRead(Object sessionToken, NIOServerIOThread base, SelectionKey sk,
ByteBuffer bs, int bytesRead) throws BadClientException
{
if (bytesRead > 0)
{
synchronized (clientSessionManagerMap)
{
TCPClientSessionManager cm = clientSessionManagerMap.get(sessionToken);
if (cm == null)
{
debug("server creating context manager for " + sessionToken);
cm = generateContextManager((String) sessionToken, sk, translationScope,
applicationObjectScope);
clientSessionManagerMap.put(sessionToken, cm);
clientSessionHandleMap.put(sessionToken, cm.getHandle());
}
try
{
CharBuffer buf = this.charBufferPool.acquire();
DECODER.decode(bs, buf, true);
buf.flip();
cm.processIncomingSequenceBufToQueue(buf);
buf = this.charBufferPool.release(buf);
}
catch (CharacterCodingException e)
{
e.printStackTrace();
}
}
synchronized (this)
{
this.notify();
}
}
}
/**
* Hook method to allow changing the ContextManager to enable specific extra functionality.
*
* @param token
* @param sc
* @param translationScopeIn
* @param registryIn
* @return
*/
@Override
protected TCPClientSessionManager generateContextManager(String sessionId, SelectionKey sk,
SimplTypesScope translationScopeIn, Scope registryIn)
{
return new ClientSessionManager(sessionId, maxMessageSize, this.getBackend(), this, sk,
translationScopeIn, registryIn);
}
@Override
public void run()
{
Iterator<TCPClientSessionManager> contextIter;
while (running)
{
synchronized (clientSessionManagerMap)
{
contextIter = clientSessionManagerMap.values().iterator();
// process all of the messages in the queues
while (contextIter.hasNext())
{
TCPClientSessionManager cm = contextIter.next();
try
{
cm.processAllMessagesAndSendResponses();
}
catch (BadClientException e)
{
// Handle BadClientException! -- remove it
error(e.getMessage());
// invalidate the manager's key
this.getBackend().setPendingInvalidate(cm.getSocketKey(), true);
// remove the manager from the collection
contextIter.remove();
}
}
}
// sleep until notified of new messages
synchronized (this)
{
try
{
wait();
}
catch (InterruptedException e)
{
e.printStackTrace();
Thread.interrupted();
}
}
}
}
/**
* @see ecologylab.generic.StartAndStoppable#start()
*/
@Override
public void start()
{
running = true;
if (t == null)
{
t = new Thread(this);
}
t.start();
super.start();
}
/**
* @see ecologylab.generic.StartAndStoppable#stop()
*/
@Override
public void stop()
{
debug("Server stopping.");
running = false;
synchronized (this)
{
this.notify();
synchronized (t)
{
t = null;
}
}
super.stop();
}
/**
* @see ecologylab.oodss.distributed.impl.Shutdownable#shutdown()
*/
@Override
public void shutdown()
{
// TODO Auto-generated method stub
}
/**
* @see ecologylab.oodss.distributed.server.NIOServerProcessor#invalidate(java.lang.Object,
* ecologylab.oodss.distributed.impl.NIOServerIOThread, java.nio.channels.SocketChannel)
*/
@Override
public boolean invalidate(String sessionId, boolean forcePermanent)
{
BaseSessionManager cm = clientSessionManagerMap.get(sessionId);
// figure out if the disconnect is permanent; will be permanent if forcing
// (usually bad client), if there is no context manager (client never sent
// data), or if the client manager says it is invalidating (client
// disconnected properly)
boolean permanent = (forcePermanent ? true : (cm == null ? true : cm.isInvalidating()));
// get the context manager...
if (permanent)
{
synchronized (clientSessionManagerMap)
{ // ...if this session will not be restored, remove the context
// manager
clientSessionManagerMap.remove(sessionId);
clientSessionHandleMap.remove(sessionId);
}
}
if (cm != null)
{
/*
* if we've gotten here, then the client has disconnected already, no reason to deal w/ the
* remaining messages // finish what the context manager was working on while
* (cm.isMessageWaiting()) { try { cm.processAllMessagesAndSendResponses(); } catch
* (BadClientException e) { e.printStackTrace(); } }
*/
cm.shutdown();
}
return permanent;
}
/**
* Attempts to switch the ContextManager for a SocketChannel. oldId indicates the session id that
* was used for the connection previously (in order to find the correct ContextManager) and
* newContextManager is the recently-created (and now, no longer necessary) ContextManager for the
* connection.
*
* @param oldId
* @param newContextManager
* @return true if the restore was successful, false if it was not.
*/
@Override
public boolean restoreContextManagerFromSessionId(String oldSessionId,
BaseSessionManager newContextManager)
{
debug("attempting to restore old session...");
TCPClientSessionManager oldContextManager;
synchronized (clientSessionManagerMap)
{
oldContextManager = this.clientSessionManagerMap.get(oldSessionId);
}
if (oldContextManager == null)
{ // cannot restore old context
debug("restore failed.");
return false;
}
else
{
oldContextManager.setSocket(newContextManager.getSocketKey());
synchronized (clientSessionManagerMap)
{
/* remove pointers to new session manager since we're using the old one */
this.clientSessionManagerMap.remove(newContextManager.getSessionId());
this.clientSessionHandleMap.remove(newContextManager.getSessionId());
}
this.getBackend().debug("old session restored!");
return true;
}
}
/**
*
* @return status of server in boolean
*/
public boolean isRunning()
{
return running;
}
@Override
protected void shutdownImpl()
{
// TODO Auto-generated method stub
}
/**
* Utility method for dynamically name TranslationScopes.
*
* @param inetAddresses
* @param portNumber
* @return
*/
protected static String connectionTscopeName(InetAddress[] inetAddresses, int portNumber)
{
return "double_threaded_logging " + inetAddresses[0].toString() + ":" + portNumber;
}
@Override
public ByteBufferPool getSharedByteBufferPool()
{
return byteBufferPool;
}
@Override
public CharBufferPool getSharedCharBufferPool()
{
return charBufferPool;
}
@Override
public StringBuilderPool getSharedStringBuilderPool()
{
return stringBuilderPool;
}
@Override
public void increaseSharedBufferPoolSize(int newCapacity) {
char[] tempCharBufferArray = Arrays.copyOf(charBufferPool.acquire().array(), charBufferPool.acquire().array().length);
byte[] tempBtyeBufferArray = Arrays.copyOf(byteBufferPool.acquire().array(), byteBufferPool.acquire().array().length);
instantiateBufferPools(newCapacity);
System.arraycopy(tempCharBufferArray, 0, charBufferPool.acquire().array(), 0, tempCharBufferArray.length);
System.arraycopy(tempBtyeBufferArray, 0, byteBufferPool.acquire().array(), 0, tempBtyeBufferArray.length);
}
}