/*
* Created on May 3, 2006
*/
package ecologylab.oodss.distributed.impl;
import java.io.IOException;
import java.net.BindException;
import java.net.InetAddress;
import java.net.InetSocketAddress;
import java.net.ServerSocket;
import java.net.Socket;
import java.net.SocketException;
import java.nio.ByteBuffer;
import java.nio.channels.SelectionKey;
import java.nio.channels.ServerSocketChannel;
import java.nio.channels.SocketChannel;
import java.security.MessageDigest;
import java.security.NoSuchAlgorithmException;
import java.util.ArrayList;
import java.util.HashMap;
import java.util.Iterator;
import java.util.LinkedList;
import java.util.Map;
import javax.xml.bind.DatatypeConverter;
import ecologylab.collections.Scope;
import ecologylab.generic.ObjectOrHashMap;
import ecologylab.oodss.distributed.common.ServerConstants;
import ecologylab.oodss.distributed.server.NIOServerDataReader;
import ecologylab.oodss.exceptions.BadClientException;
import ecologylab.serialization.SimplTypesScope;
/**
* The backend portion of the NIO Server, which handles low-level communication with clients.
*
* Re-written based on the Rox Java NIO Tutorial
* (http://rox-xmlrpc.sourceforge.net/niotut/index.html).
*
* @author Zachary O. Toups (zach@ecologylab.net)
*
*/
public class NIOServerIOThread extends NIONetworking implements ServerConstants
{
static NIOServerIOThread getInstance(int portNumber, InetAddress[] hostAddresses,
NIOServerDataReader sAP, SimplTypesScope requestTranslationSpace, Scope<?> objectRegistry,
int idleSocketTimeout, int maxMessageLength) throws IOException, BindException
{
return new NIOServerIOThread(portNumber, hostAddresses, sAP, requestTranslationSpace,
objectRegistry, idleSocketTimeout, maxMessageLength);
}
private final ArrayList<ServerSocket> incomingConnectionSockets = new ArrayList<ServerSocket>();
private NIOServerDataReader sAP;
private int idleSocketTimeout;
private Map<SelectionKey, Long> keyActivityTimes = new HashMap<SelectionKey, Long>();
private Map<String, ObjectOrHashMap<String, SelectionKey>> ipToKeyOrKeys = new HashMap<String, ObjectOrHashMap<String, SelectionKey>>();
private boolean acceptEnabled = false;
private MessageDigest digester;
private long dispensedTokens;
private InetAddress[] hostAddresses;
private final ArrayList<InetAddress> boundAddresses = new ArrayList<InetAddress>();
protected NIOServerIOThread(int portNumber, InetAddress[] hostAddresses, NIOServerDataReader sAP,
SimplTypesScope requestTranslationSpace, Scope<?> objectRegistry, int idleSocketTimeout,
int maxMessageLength) throws IOException, BindException
{
super("NIOServer", portNumber, requestTranslationSpace, objectRegistry, maxMessageLength);
this.construct(hostAddresses, sAP, idleSocketTimeout);
}
private void construct(InetAddress[] newHostAddresses, NIOServerDataReader newFrontend,
int newIdleSocketTimeout) throws IOException
{
this.hostAddresses = newHostAddresses;
this.sAP = newFrontend;
this.idleSocketTimeout = newIdleSocketTimeout;
try
{
digester = MessageDigest.getInstance("SHA-256");
}
catch (NoSuchAlgorithmException e)
{
weird("This can only happen if the local implementation does not include the given hash algorithm.");
e.printStackTrace();
}
}
/**
* Gets all host addresses associated with this server.
*
* @return
*/
public InetAddress[] getHostAddresses()
{
return hostAddresses;
}
/**
* Checks all of the current keys to see if they have been idle for too long and drops them if
* they have.
*
*/
@Override
protected void checkAndDropIdleKeys()
{
LinkedList<SelectionKey> keysToInvalidate = new LinkedList<SelectionKey>();
long timeStamp = System.currentTimeMillis();
if (idleSocketTimeout > -1)
{ /*
* after we select, we'll check to see if we need to boot any idle keys
*/
for (SelectionKey sKey : keyActivityTimes.keySet())
{
if ((timeStamp - keyActivityTimes.get(sKey)) > idleSocketTimeout)
{
keysToInvalidate.add(sKey);
}
}
}
else
{ /*
* We have to clean up the key set at some point; use GARBAGE_CONNECTION_CLEANUP_TIMEOUT
*/
for (SelectionKey sKey : keyActivityTimes.keySet())
{
if ((timeStamp - keyActivityTimes.get(sKey)) > GARBAGE_CONNECTION_CLEANUP_TIMEOUT)
{
keysToInvalidate.add(sKey);
}
}
}
// remove all the invalid keys
for (SelectionKey keyToInvalidate : keysToInvalidate)
{
debug(keyToInvalidate.attachment() + " took too long to request; disconnecting.");
keyActivityTimes.remove(keyToInvalidate);
this.setPendingInvalidate(keyToInvalidate, true);
}
}
/**
* Accept an incoming connection from a client to a server, if the server has connections
* available and the client is not bad. Generate a session identifier and attach it to the
* newly-connected client's SelectionKey, so that a client session manager can be associated with
* the connection.
*/
@Override
protected final void acceptKey(SelectionKey key)
{
try
{
int numConn = selector.keys().size() - 1;
debug("connections running: " + numConn);
if (numConn < MAX_CONNECTIONS)
{ // the keyset includes this side of the connection
if (numConn - 1 == MAX_CONNECTIONS)
{
debug("Maximum connections reached; disabling accept until a client drops.");
for (ServerSocket s : this.incomingConnectionSockets)
{
SelectionKey closingKey = s.getChannel().keyFor(this.selector);
closingKey.cancel();
s.close();
closingKey.channel().close();
}
acceptEnabled = false;
}
SocketChannel newlyAcceptedChannel = ((ServerSocketChannel) key.channel()).accept();
InetAddress address = newlyAcceptedChannel.socket().getInetAddress();
debug("new address: " + address.getHostAddress());
if (!BadClientException.isEvilHostByNumber(address.getHostAddress()))
{
newlyAcceptedChannel.configureBlocking(false);
// when we register, we want to attach the proper
// session token to all of the keys associated with
// this connection, so we can sort them out later.
String keyAttachment = this.generateSessionToken(newlyAcceptedChannel.socket());
SelectionKey newKey = newlyAcceptedChannel.register(selector, SelectionKey.OP_READ,
keyAttachment);
this.keyActivityTimes.put(newKey, System.currentTimeMillis());
if ((ipToKeyOrKeys.get(address.getHostAddress())) == null)
{
debug(address + " not in our list, adding it.");
ipToKeyOrKeys.put(address.getHostAddress(), new ObjectOrHashMap<String, SelectionKey>(
keyAttachment, newKey));
}
else
{
debug(address + " is in our list, adding another key.");
synchronized (ipToKeyOrKeys)
{
ipToKeyOrKeys.get(address.getHostAddress()).put(keyAttachment, newKey);
System.out.println("new size: " + ipToKeyOrKeys.get(address.getHostAddress()).size());
}
}
debug("Now connected to "
+ newlyAcceptedChannel
+ ", "
+ (MAX_CONNECTIONS - numConn - 1)
+ " connections remaining.");
return;
}
}
// we will prematurely exit before now if it's a good connection
// so now it's a bad one; disconnect it and return null
SocketChannel tempChannel = ((ServerSocketChannel) key.channel()).accept();
InetAddress address = null;
if (tempChannel != null && tempChannel.socket() != null)
{
address = tempChannel.socket().getInetAddress();
// shut it all down
tempChannel.socket().shutdownInput();
tempChannel.socket().shutdownOutput();
tempChannel.socket().close();
tempChannel.close();
}
// show a debug message
if (numConn >= MAX_CONNECTIONS)
debug("Rejected connection; already fulfilled max connections.");
else
debug("Evil host attempted to connect: " + address);
}
catch (IOException e)
{
e.printStackTrace();
}
catch (NullPointerException e)
{
e.printStackTrace();
}
}
/**
* @see ecologylab.oodss.distributed.impl.NIONetworking#removeBadConnections()
*/
@Override
protected void removeBadConnections(SelectionKey key)
{
// shut them ALL down!
InetAddress address = ((SocketChannel) key.channel()).socket().getInetAddress();
ObjectOrHashMap<String, SelectionKey> keyOrKeys = ipToKeyOrKeys.get(address.getHostAddress());
Iterator<SelectionKey> allKeysForIp = keyOrKeys.values().iterator();
debug("***********Shutting down all clients from " + address.getHostAddress());
while (allKeysForIp.hasNext())
{
SelectionKey keyForIp = allKeysForIp.next();
debug("shutting down " + ((SocketChannel) keyForIp.channel()).socket().getInetAddress());
this.setPendingInvalidate(keyForIp, true);
}
keyOrKeys.clear();
ipToKeyOrKeys.remove(address.getHostAddress());
}
/**
* Handles the client manager for the socket (remove if permanent) and shuts down communication on
* the socket.
*
* @see ecologylab.oodss.distributed.impl.NIONetworking#invalidateKey(java.nio.channels.SocketChannel,
* boolean)
*/
@Override
protected void invalidateKey(SelectionKey key, boolean permanent)
{
SocketChannel chan = (SocketChannel) key.channel();
InetAddress address = chan.socket().getInetAddress();
sAP.invalidate((String) key.attachment(), permanent);
super.invalidateKey(chan);
ObjectOrHashMap<String, SelectionKey> keyOrKeys = this.ipToKeyOrKeys.get(address
.getHostAddress());
if (keyOrKeys != null)
{
keyOrKeys.remove(address);
if (keyOrKeys.isEmpty())
{
this.ipToKeyOrKeys.remove(chan.socket().getInetAddress().getHostAddress());
}
}
this.keyActivityTimes.remove(key);
/*
* decrement numConnections & if the server disabled new connections due to hitting
* max_connections, re-enable
*/
if (selector.keys().size() < MAX_CONNECTIONS && !acceptEnabled)
{
try
{
this.registerAcceptWithSelector();
}
catch (IOException e)
{
debug("Unable to re-open socket for accepts; critical failure.");
e.printStackTrace();
}
}
}
/**
* Attempts to bind all of the ports in the hostAddresses array. If a port cannot be bound, it is
* removed from the hostAddresses array.
*
* @throws IOException
*/
void registerAcceptWithSelector() throws IOException
{
boundAddresses.clear();
incomingConnectionSockets.clear();
for (int i = 0; i < hostAddresses.length; i++)
{
debug("setting up accept on " + hostAddresses[i] + ": " + portNumber);
// acquire the static ServerSocketChannel object
ServerSocketChannel channel = ServerSocketChannel.open();
// disable blocking
channel.configureBlocking(false);
try
{
ServerSocket newSocket = channel.socket();
// get the socket associated with the channel
// bind to the port for this server
newSocket.bind(new InetSocketAddress(hostAddresses[i], portNumber));
newSocket.setReuseAddress(true);
channel.register(this.selector, SelectionKey.OP_ACCEPT);
this.incomingConnectionSockets.add(newSocket);
this.boundAddresses.add(hostAddresses[i]);
}
catch (BindException e)
{
debug("Unable to bind " + hostAddresses[i]);
debug(e.getMessage());
e.printStackTrace();
}
catch (SocketException e)
{
System.err.println(e.getMessage());
}
}
if (this.boundAddresses.size() == 0)
{
throw new BindException("Server was unable to bind to any addresses.");
}
// register the channel with the selector to look for incoming
// accept requests
acceptEnabled = true;
}
/**
* Generates a unique identifier String for the given socket, based upon actual ports used and ip
* addresses with a hash. Called by the server at accept() time, and used to identify the
* connection thereafter.
*
* @param incomingSocket
* @return
*/
protected String generateSessionToken(Socket incomingSocket)
{
// clear digester
digester.reset();
// we make a string consisting of the following:
// time of initial connection (when this method is called),
// client ip, client actual port
digester.update(String.valueOf(System.currentTimeMillis()).getBytes());
// digester.update(String.valueOf(System.nanoTime()).getBytes());
// digester.update(this.incomingConnectionSockets[0].getInetAddress()
// .toString().getBytes());
digester.update(incomingSocket.getInetAddress().toString().getBytes());
digester.update(String.valueOf(incomingSocket.getPort()).getBytes());
digester.update(String.valueOf(this.dispensedTokens).getBytes());
dispensedTokens++;
// convert to normal characters and return as a String
return DatatypeConverter.printBase64Binary(digester.digest());
}
@Override
protected void close()
{
try
{
debug("Closing selector.");
selector.close();
debug("Unbinding.");
for (ServerSocket s : incomingConnectionSockets)
{
s.close();
}
}
catch (IOException e)
{
e.printStackTrace();
}
}
@Override
protected void processReadData(Object sessionToken, SelectionKey sk, ByteBuffer bytes,
int bytesRead) throws BadClientException
{
this.sAP.processRead(sessionToken, this, sk, bytes, bytesRead);
this.keyActivityTimes.put(sk, System.currentTimeMillis());
}
/**
* @see ecologylab.oodss.distributed.impl.NIOCore#acceptReady(java.nio.channels.SelectionKey)
*/
@Override
protected void acceptReady(SelectionKey key)
{
this.acceptKey(key);
}
/**
* @see ecologylab.oodss.distributed.impl.NIOCore#connectReady(java.nio.channels.SelectionKey)
*/
@Override
protected void connectReady(SelectionKey key)
{
}
/**
* @see ecologylab.oodss.distributed.impl.NIOCore#readFinished(java.nio.channels.SelectionKey)
*/
@Override
protected void readFinished(SelectionKey key)
{
}
/**
* @param socket
* @param permanent
*/
public void setPendingInvalidate(SocketChannel socket, boolean permanent)
{
this.setPendingInvalidate(socket.keyFor(selector), permanent);
}
/**
* @see ecologylab.oodss.distributed.impl.NIOCore#acceptFinished(java.nio.channels.SelectionKey)
*/
@Override
public void acceptFinished(SelectionKey key)
{
}
@Override
protected boolean handleInvalidate(SelectionKey key, boolean forcePermanent)
{
return this.sAP.invalidate((String) key.attachment(), forcePermanent);
}
/**
* @return the boundAddresses
*/
public ArrayList<InetAddress> getBoundAddresses()
{
return boundAddresses;
}
}