package org.yajul.net;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
import javax.security.auth.DestroyFailedException;
import java.net.InetAddress;
import java.net.ServerSocket;
import java.net.Socket;
import java.net.SocketException;
import java.io.IOException;
import java.util.ArrayList;
import java.util.List;
import java.util.concurrent.ExecutorService;
import java.util.concurrent.atomic.AtomicInteger;
import java.util.concurrent.locks.Condition;
import java.util.concurrent.locks.Lock;
import java.util.concurrent.locks.ReentrantLock;
/**
* Manages a set of client connections accepted via a server socket. Provides
* a base class for TCP/IP servers of various types.
* User: jdavis
* Date: Dec 11, 2003
* Time: 11:08:10 AM
*
* @author jdavis
*/
public class SocketListener {
private static Logger log = LoggerFactory.getLogger(SocketListener.class);
/**
* Set max connections to this value to provide unlimited connections. Note:
* if this is set, the VM may encounter problems when too many threads are
* started.
*/
public final static int UNLIMITED_CONNECTIONS = Integer.MAX_VALUE;
/**
* The default log size is 20.
*/
public final static int DEFAULT_BACKLOG = 20;
/**
* The default connection timeout.
*/
public final static int DEFAULT_CONNECTION_TIMEOUT = 900000;
public static final int WAIT_TIMEOUT = 5000;
/**
* The server socket to listen on.
*/
private final ServerSocket socket;
/**
* The thread pool to use for incoming connections and the main listener.
*/
private final ExecutorService executor;
/**
* Active connections to clients.
*/
private final List<ClientConnection> clientConnections = new ArrayList<ClientConnection>();
/**
* True if a shutdown has been requested.
*/
private boolean shutdownRequested = false;
/**
* The maximum number of connections the server will accept.
*/
private int maxConnections = UNLIMITED_CONNECTIONS;
/**
* True if clients should be automatically disconnected when
* the maximum number of client connections is reached. False
* will cause the server to wait until there is an available connection
* (a.k.a. 'rude mode').
*/
private boolean rejectIfUnavailable = false;
/**
* The socket timeout for client connections.
*/
private int connectionTimeout = DEFAULT_CONNECTION_TIMEOUT;
private ClientTaskFactory clientTaskFactory;
private Lock lock = new ReentrantLock();
private Condition listening = lock.newCondition();
/**
* Creates a new server socket listener on the specified port.
*
*
* @param port The IP port to listen on.
* @throws IOException if something goes wrong.
*/
public SocketListener(InetAddress bindAddress, int port,
ExecutorService executorService,
ClientTaskFactory clientTaskFactory,
int backlog) throws IOException {
this.executor = executorService;
this.clientTaskFactory = clientTaskFactory;
socket = new ServerSocket(port,backlog,bindAddress);
}
/**
* Creates a new server socket listener on the specified port.
*
*
* @param port The IP port to listen on.
* @throws IOException if something goes wrong.
*/
public SocketListener(InetAddress bindAddress, int port,
ExecutorService executorService,
ClientTaskFactory clientTaskFactory) throws IOException {
this(bindAddress,port,executorService,clientTaskFactory, DEFAULT_BACKLOG);
}
/**
* Returns the port number this server is listening on.
*
* @return The port number this server is listening on.
*/
public int getPort() {
return socket.getLocalPort();
}
public ExecutorService getExecutor() {
return executor;
}
public void start() {
synchronized (this) {
if (shutdownRequested)
throw new IllegalStateException("Shutdown requested! Cannot start!");
executor.submit(new Runnable() {
public void run() {
listenerLoop();
}
});
}
lock.lock();
try {
listening.await();
log.info("Started.");
} catch (InterruptedException e) {
log.warn("Interrupted: " + e);
} finally {
lock.unlock();
}
}
/**
* Stops the server, and any active clients.
*/
public void shutdown() {
synchronized (clientConnections) {
for (ClientConnection connection : clientConnections) {
try {
connection.shutdown();
} catch (Throwable e) {
unexpected(e);
}
}
clientConnections.clear();
clientConnections.notifyAll();
}
shutdownRequested = true;
try {
socket.close();
log.info("Socket closed.");
} catch (IOException ioex) {
unexpected(ioex);
}
}
/**
* Returns true if client connections should be closed if the maximum
* number of clients has been reached.
*
* @return true if client connections should be closed if the maximum
* number of clients has been reached.
*/
public boolean isRejectIfUnavailable() {
synchronized (this) {
return rejectIfUnavailable;
}
}
/**
* Enables or disables 'rude' treatment of clients when the maximum number
* of clients has been reached. If enabled, incoming client connections
* will be closed when the maximum number of clients has been reached.
*
* @param rejectIfUnavailable true if incoming clients are to be rejected when the server is busy
*/
public void setRejectIfUnavailable(boolean rejectIfUnavailable) {
synchronized (this) {
this.rejectIfUnavailable = rejectIfUnavailable;
}
}
/**
* Clients are required to call this method when they shut down.
*
* @param client The client that has stopped.
*/
void clientClosed(ClientConnection client) {
removeClient(client);
}
/**
* Returns the client socket connection timeout.
*
* @return The client socket connection timeout.
*/
public int getConnectionTimeout() {
return connectionTimeout;
}
public void setConnectionTimeout(int timeout) {
this.connectionTimeout = timeout;
}
/**
* Handle an unexpected exception.
*
* @param t The unexpected exception.
*/
protected void unexpected(Throwable t) {
Logger logger = LoggerFactory.getLogger(this.getClass());
logger.error("Unexpected: " + t,t);
}
/**
* Returns true if the listener should accept the client, false if not.
* Subclasses can override this to add their own behavior.
* @param incoming the incoming client socket
* @return true if the listener should accept the client, false if not.
*/
@SuppressWarnings({"UnusedParameters"})
protected boolean shouldAccept(Socket incoming) {
return true;
}
private void doAccept(Socket incoming) throws IOException {
final ClientConnection client = new ClientConnection(this,incoming);
synchronized (clientConnections) {
clientConnections.add(client);
log.info("Client connection " + client + " added.");
clientConnections.notifyAll();
}
try {
// NOTE: The client *must not* start any threads until this method is called!
client.start();
if (log.isDebugEnabled())
log.debug("doAccept() : Client connection handler started.");
} catch (Throwable t) {
log.error("Unable to start client due to: " + t, t);
try {
client.close();
} catch (Throwable e) {
log.error("Unable to close client due to: " + e, e);
}
}
}
private boolean removeClient(ClientConnection client) {
synchronized (clientConnections) {
boolean found = clientConnections.remove(client);
if (found)
log.info("Client connection " + client + " removed.");
clientConnections.notifyAll();
return found;
}
}
private synchronized void setShutdownRequested(boolean shutdownRequested) {
this.shutdownRequested = shutdownRequested;
}
private synchronized boolean isShutdownRequested() {
return shutdownRequested;
}
ClientTaskFactory getClientTaskFactory() {
return clientTaskFactory;
}
private void listenerLoop() {
log.info("BEGIN - Listener on port " + getPort());
while (!isShutdownRequested()) {
try {
boolean reject = false;
synchronized (clientConnections) {
while ((clientConnections.size() > maxConnections)
&& (!isShutdownRequested())) {
// If 'rude mode' is enabled, kick the client.
// Don't bother looping either.
if (isRejectIfUnavailable()) {
reject = true;
break;
}
// Otherwise, wait...
else {
log.info("Client connection limit reached, waiting...");
try {
clientConnections.wait(WAIT_TIMEOUT);
} catch (InterruptedException e) {
unexpected(e);
}
}
} // while
} // synchronized
if (log.isDebugEnabled())
log.debug("listenerLoop() : Waiting for a connection...");
// Accept a socket connection...
lock.lock();
try {
listening.signalAll();
} finally {
lock.unlock();
}
Socket incoming = socket.accept();
if (log.isDebugEnabled())
log.debug("listenerLoop() : Client " + incoming.getInetAddress());
// If we're not already rejecting this connection, filter it...
if (!reject) {
reject = !shouldAccept(incoming);
}
if (reject) {
incoming.close();
log.error("Socket connection from " + incoming.getInetAddress() + " rejected!");
continue; // Continue accepting other connections.
}
// Accept this client.
doAccept(incoming);
} catch (SocketException e) {
log.warn("SocketException: " + e);
/* 2004-02-25 [jsd] In JDK1.3, Socket doesn't have the isClosed() method.
if (socket.isClosed() && shutdownRequested)
log.info("Listener on port " + port + ", server socket closed.");
else
unexpected(e);
*/
if (isShutdownRequested())
log.info("Listener on port " + getPort() + ", server socket closed.");
else
unexpected(e);
break;
} catch (Throwable e) {
unexpected(e);
break;
}
} // while
setShutdownRequested(false);
log.info("END - Listener on port " + getPort());
}
}