/*********************************************************************
* EJServer.java
* created on 06.08.2004 by netseeker
* $Source: /cvsroot/ejoe/EJOE/src/de/netseeker/ejoe/EJServer.java,v $
* $Date: 2007/11/17 10:59:40 $
* $Revision: 1.78 $
*
* ====================================================================
*
* Copyright 2005-2006 netseeker aka Michael Manske
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
* ====================================================================
*
* This file is part of the ejoe framework.
* For more information on the author, please see
* <http://www.manskes.de/>.
*
*********************************************************************/
package de.netseeker.ejoe;
import java.io.FileInputStream;
import java.io.IOException;
import java.net.InetSocketAddress;
import java.nio.channels.ServerSocketChannel;
import java.util.Properties;
import java.util.logging.Level;
import java.util.logging.Logger;
import java.util.zip.Deflater;
import de.netseeker.ejoe.core.ChannelRegistrar;
import de.netseeker.ejoe.core.CombinedConnectionProcessor;
import de.netseeker.ejoe.core.ConnectionAcceptor;
import de.netseeker.ejoe.core.EJServerRegistry;
import de.netseeker.ejoe.handler.ClassHandler;
import de.netseeker.ejoe.handler.PingHandler;
import de.netseeker.ejoe.handler.ServerHandler;
import de.netseeker.ejoe.handler.ServerHandlerMapping;
import de.netseeker.ejoe.io.IOUtil;
import de.netseeker.ejoe.jmx.EJServerConfig;
import de.netseeker.ejoe.jmx.EJServerConfigMBean;
import de.netseeker.ejoe.request.ClassloaderRequest;
import de.netseeker.ejoe.request.PingRequest;
/**
* This is the server component of EJOE. EJOE is a request object broker in it's natural meaning. You have to use this
* component if you want retrieve and send data from/to EJOE clients. EJOE offers basically three things for you:
* <ol>
* <li>a multithreaded, high performance io server</li>
* <li>(de)serializing input of input objects send by clients and return objects provided by YOUR business logic</li>
* <li>a simple, clean and unique interface to integrate a object request broker into your application</li>
* </ol>
* <p>
* Basic usage:
*
* <pre>
* EJServer server = new EJServer( new EchoHandler() );
* //to allow persistent client connections use
* //server.enablePersistentConnections( true );
*
* //to use old style blocking IO instead of NIO use
* //server.enableNonBlockingIO( false );
*
* server.start();
* ...
* server.stop();
* </pre>
*
* </p>
*
* @author netseeker aka Michael Manske
* @since 0.3.0
*/
public class EJServer
{
private transient static final Logger logger = Logger.getLogger( EJServer.class.getName() );
private transient ConnectionAcceptor _acceptor;
private transient ChannelRegistrar[] _processors;
private transient ServerSocketChannel _channel;
private ServerInfo _serverInfo = new ServerInfo();
private boolean _isShuttingDown = false;
private EJServerConfigMBean _jmxConfigBean;
/**
* Creates an instance of the EJOE server component pre-configured whith a default value for the used port
*
* @param handler an implementation of de.netseeker.ejoe.ServerHandler
*/
public EJServer(final ServerHandler handler)
{
this( handler, EJConstants.EJOE_PORT );
}
/**
* Creates an instance of the EJOE server component pre-configured whith a default value for the used port
*
* @param handler an implementation of de.netseeker.ejoe.ServerHandler
* @param bindAddr the IP address identifying the network interface to which EJServer should bind itself
*/
public EJServer(final ServerHandler handler, String bindAddr)
{
this( handler, bindAddr, EJConstants.EJOE_PORT );
}
/**
* Creates an instance of the EJOE server component
*
* @param handler an implementation of de.netseeker.ejoe.ServerHandler
* @param port the port EJOE should listen to
*/
public EJServer(final ServerHandler handler, int port)
{
ServerHandlerMapping mapping = null;
if ( handler instanceof ServerHandlerMapping )
{
mapping = (ServerHandlerMapping) handler;
}
else
{
mapping = new ServerHandlerMapping();
mapping.setDefaultHandler( handler );
}
mapping.addHandlerMapping( PingRequest.UNIQUE_NAME, new PingHandler() );
_serverInfo.setHandler( handler );
_serverInfo.setPort( port );
}
/**
* Creates an instance of the EJOE server component
*
* @param handler an implementation of de.netseeker.ejoe.ServerHandler
* @param bindAddr the IP address identifying the network interface to which EJServer should bind itself
* @param port the port EJOE should listen to
*/
public EJServer(final ServerHandler handler, String bindAddr, int port)
{
this( handler, port );
_serverInfo.setInterface( bindAddr );
}
/**
* Creates an instance of EJServer pre-configured with settings from the global ejserver.properties file. You MUST
* provide such an property file on the classpath to use this constructor.
*/
public EJServer()
{
Properties props = new Properties();
try
{
props.load( EJServer.class.getResourceAsStream( "/ejserver.properties" ) );
loadProperties( props );
}
catch ( IOException e )
{
logger.log( Level.SEVERE, "ejserver.properties could not be read! Make sure you have placed"
+ " a valid properties file in your classpath.", e );
throw new RuntimeException( e );
}
}
/**
* Creates an instance of EJServer pre-configured with settings from the given properties store
*
* @param properties properties store containing EJServer settings
*/
public EJServer(Properties properties)
{
loadProperties( properties );
}
/**
* Creates an instance of EJServer pre-configured with settings from a global properties file.
*
* @param pathToConfigFile path to the properties file
*/
public EJServer(String pathToConfigFile)
{
Properties props = new Properties();
FileInputStream fis = null;
try
{
fis = new FileInputStream( pathToConfigFile );
props.load( fis );
loadProperties( props );
}
catch ( IOException e )
{
logger.log( Level.SEVERE, "ejserver.properties could not be read! Make sure you have placed"
+ " a valid properties file in your classpath.", e );
throw new RuntimeException( e );
}
finally
{
IOUtil.closeQuiet( fis );
}
}
/**
* Indicates if this EJServer is running and ready to accept and process incoming connections
*
* @return true if this EJServer is running and ready to accept and process incoming connections otherwise false
*/
public boolean isRunning()
{
return (_processors != null && _acceptor != null && _acceptor.isAlive() && !_isShuttingDown);
}
/**
* Returns the current amount of supported concurrent read processor threads.
*
* @return
*/
public int getMaxReadProcessors()
{
return _serverInfo.getMaxReadProcessors();
}
/**
* Sets the amount of threads used for processing read operations on accepted connections. This will indirectly
* control the amount of concurrently processed io read operations and adapter calls.
*
* @param maxProcessors new amount of threads used for processing accepted connections
* @see #setMaxWriteProcessors(int)
*/
public void setMaxReadProcessors( int maxProcessors )
{
if ( !isRunning() )
{
_serverInfo.setMaxReadProcessors( maxProcessors );
}
else
{
throw new IllegalStateException( "The value for the maximum of used read processor"
+ "threads can't be changed while EJOE is already running!" );
}
}
/**
* Returns the current amount of supported concurrent write processor threads.
*
* @return
*/
public int getMaxWriteProcessors()
{
return _serverInfo.getMaxWriteProcessors();
}
/**
* Sets the amount of threads used for processing write operations. This will directly control the amount of
* concurrently processed io socket write operations.
*
* @param maxProcessors new amount of threads used for processing io socket write operations
* @see #setMaxReadProcessors(int)
*/
public void setMaxWriteProcessors( int maxProcessors )
{
if ( !isRunning() )
{
_serverInfo.setMaxWriteProcessors( maxProcessors );
}
else
{
throw new IllegalStateException(
"The value for the maximum of used write processor threads can't be changed "
+ "while EJOE is already running!" );
}
}
/**
* Enables/Disables automic, periodic control of the used worker pools. If enabled the worker pools will be checked
* regulary and shrinked or increased if neccessary. The control ensures that the pool sizes will never extend the
* size of maxReadProcessors/maxWriteProcessors.
*
* @param enable
* @see #enableThreadPoolResizeControl(boolean, long)
*/
public void enableThreadPoolResizeControl( boolean enable )
{
if ( _processors == null )
{
_serverInfo.setAutomaticThreadPoolResize( enable );
}
else
{
throw new IllegalStateException( "ThreadPoolResizeControl can't be changed"
+ " while EJOE is already running!" );
}
}
/**
* This method works exactly as {@link #enableThreadedProcessorUsage(boolean)} but allows setting of a custom time
* period between control checks of the used worker pools.
*
* @param enable
* @param controlPeriod period between control checks in milliseconds
* @see #enableThreadPoolResizeControl(boolean)
*/
public void enableThreadPoolResizeControl( boolean enable, long controlPeriod )
{
if ( _processors == null )
{
_serverInfo.setAutomaticThreadPoolResize( enable );
_serverInfo.setPoolResizePeriod( controlPeriod );
}
else
{
throw new IllegalStateException( "ThreadPoolResizeControl can't be changed"
+ " while EJOE is already running!" );
}
}
/**
* Returns whether compression has been enabled or not.
*
* @return
*/
public boolean hasCommpression()
{
return _serverInfo.hasCompression();
}
/**
* Enables or disables the usage of compressing/decompressing for outgoing/incoming data. Enabling compression *can*
* result in signifcantly better throughput especially when using a text based SerializeAdapter in confunction with
* large data objects.
*
* @see de.netseeker.ejoe.adapter.SerializeAdapter
*/
public void enableCompression( boolean enable )
{
this._serverInfo.setCompression( enable );
}
/**
* Enables the usage of compressing/decompressing for outgoing/incoming data with the given compression level.
* Enabling compression *can* result in signifcantly better throughput especially when using a text based
* SerializeAdapter in confunction with large data objects.
*
* @param compressionLevel the level of compression to use, must be in range of 0-9
* @see de.netseeker.ejoe.adapter.SerializeAdapter
*/
public void enableCompression( int compressionLevel )
{
if ( compressionLevel < Deflater.NO_COMPRESSION || compressionLevel > Deflater.BEST_COMPRESSION )
{
throw new IllegalArgumentException( "Compressionlevel must be in the range of " + Deflater.NO_COMPRESSION
+ ':' + Deflater.BEST_COMPRESSION );
}
this._serverInfo.setCompression( true );
this._serverInfo.setCompressionLevel( compressionLevel );
}
/**
* Returns whether NIO has been enabled or not.
*
* @return
*/
public boolean hasNonBlockingIO()
{
return this._serverInfo.hasNonBlockingReadWrite();
}
/**
* Enables/disables new style non-blocking IO for read and write operations. Sometimes this can be significantly
* faster than using blocking io. Be aware that connection acception as well as EJOES internal connection
* handshaking will use NIO anyway to ensure high latency.
*/
public void enableNonBlockingIO( boolean enable )
{
if ( enable || (!enable && !hasHttPackaging()) )
{
this._serverInfo.setNonBlockingReadWrite( enable );
}
else
{
throw new IllegalStateException(
"You have tried to disable non-blocking IO while HTTP support is still active! "
+ "HTTP support is only implemented for non-blocking IO. "
+ "To disable non-blocking IO you must disable HTTP packaging first." );
}
}
/**
* Returns whether support for persistent connections has been enabled or not.
*
* @return
*/
public boolean hasPersistentConnections()
{
return this._serverInfo.isPersistent();
}
/**
* Enables/disables support for persistents client connections. Usage of persistent connections is more performant
* in most cases because the client doesn't need to open a new connection on each request. The drawback of
* persistent connections can be a higher server load because the server may have to handle a lot of "idle" client
* connections.
*
* @param enable
*/
public void enablePersistentConnections( boolean enable )
{
this._serverInfo.setPersistent( enable );
}
/**
* Enables/disables support for http packaging.
*
* @param enable
*/
public void enableHttpPackaging( boolean enable )
{
if ( (enable && hasNonBlockingIO()) || !enable )
{
this._serverInfo.setHttp( enable );
}
else
{
throw new IllegalStateException(
"HTTP support is only implemted for non-blocking IO! Enable non-blocking IO if you want to use HTTP packaging..." );
}
}
/**
* Returns whether support for HTTP packaging has been enabled or not.
*
* @return
*/
public boolean hasHttPackaging()
{
return this._serverInfo.isHttp();
}
/**
* Returns the number of Connection Processors used for delegating network IO operations to reader/writer workers
*
* @return the number of used Connection Processors
*/
public int getConnectionProcessors()
{
return this._serverInfo.getTargetedConnectionProcessors();
}
/**
* Sets the number of Connection Processors to use for delegating network IO operations to reader/writer workers. It
* is strongly recommended to use the number of physically available processor units.
*
* @param count the number of Connection Processors to use
*/
public void setConnectionProcessors( int count )
{
if ( !isRunning() )
{
this._serverInfo.setTargetedConnectionProcessors( count );
}
else
{
throw new IllegalStateException( "The number of Connection Processors must not be changed "
+ "while EJServer is already running!" );
}
}
/**
* Enables support for remote classloading
*
* @param enable
*/
public void enableRemoteClassLoading( boolean enable )
{
if ( !isRunning() )
{
_serverInfo.setClassServerEnabled( enable );
ServerHandlerMapping mapping = (ServerHandlerMapping) _serverInfo.getHandler();
if ( enable )
{
mapping.addHandlerMapping( ClassloaderRequest.UNIQUE_NAME, new ClassHandler() );
}
else
{
mapping.removeHandlerMapping( ClassloaderRequest.UNIQUE_NAME );
}
}
else
{
throw new IllegalStateException( "Remote classloading can't be changed" + " while EJOE is already running!" );
}
}
/**
* Returns a JMX compliant bean to configure EJServer via JMX
*
* @return
*/
public EJServerConfigMBean getJMXConfigurationBean()
{
if ( this._jmxConfigBean == null )
{
this._jmxConfigBean = new EJServerConfig( this );
}
return this._jmxConfigBean;
}
/**
* @return
*/
public IServerInfo getServerInfo()
{
return this._serverInfo;
}
/**
* (Re)Starts the main server as well as the class loader server (if it's configured)
*/
public void start() throws IOException
{
logger.log( Level.INFO, "Starting EJOE server..." );
if ( _serverInfo.isServerRunning() )
{
logger.log( Level.WARNING, "EJOE server already running - will try a restart." );
stop();
}
_isShuttingDown = false;
ConnectionAcceptor acceptor = null;
ChannelRegistrar processors[] = new ChannelRegistrar[this._serverInfo.getTargetedConnectionProcessors()];
ServerSocketChannel channel = null;
try
{
channel = ServerSocketChannel.open();
channel.configureBlocking( false );
channel.socket().setReuseAddress( true );
InetSocketAddress address = new InetSocketAddress( _serverInfo.getInterface(), _serverInfo.getPort() );
channel.socket().bind( address, 1024 );
this._serverInfo.setHost( address.getHostName() + ':' + address.getPort() );
// this._connectionHeader.setChannel(channel);
for ( int i = 0; i < processors.length; i++ )
{
processors[i] = new CombinedConnectionProcessor( this._serverInfo );
((Thread) processors[i]).setDaemon( true );
}
acceptor = new ConnectionAcceptor( channel, processors );
//acceptor.setDaemon( true );
}
catch ( IOException e )
{
logger.log( Level.SEVERE, "!!! IOException occured !!! ", e );
IOUtil.closeQuiet( channel );
throw (e);
}
for ( int i = 0; i < processors.length; i++ )
{
((CombinedConnectionProcessor) processors[i]).start();
}
acceptor.start();
this._serverInfo.setServerRunning( true );
this._processors = processors;
this._acceptor = acceptor;
this._channel = channel;
EJServerRegistry.getInstance().register( this );
if ( logger.isLoggable( Level.INFO ) )
{
logger.log( Level.INFO, "EJOE server listening on: " + channel.socket().getLocalSocketAddress() );
logger.log( Level.INFO, "Using " + processors.length + " Connection Processor"
+ (processors.length > 1 ? "s" : "") );
logger.log( Level.INFO, "Using non-blocking IO: " + this._serverInfo.hasNonBlockingReadWrite() );
logger.log( Level.INFO, "Allowing persistent client connections: " + this._serverInfo.isPersistent() );
logger.log( Level.INFO, "Using compression: " + this._serverInfo.hasCompression() );
logger.log( Level.INFO, "Supporting HTTP tunneling: " + this._serverInfo.isHttp() );
logger.log( Level.INFO, "Supporting remote classloading: " + this._serverInfo.isClassServerEnabled() );
logger.log( Level.INFO, "Using automatic thread pool resizing: "
+ this._serverInfo.isAutomaticThreadPoolResize() );
logger.log( Level.INFO, "EJOE server started successfully." );
}
}
/**
* Stops the main server as well as the class loader server (if it's running)
*/
public void stop()
{
logger.log( Level.INFO, "Stopping EJOE server..." );
_isShuttingDown = true;
EJServerRegistry.getInstance().deRegister( this );
if ( isRunning() )
{
this._acceptor.interrupt();
for ( int i = 0; i < this._processors.length; i++ )
{
((Thread) this._processors[i]).interrupt();
}
this._serverInfo.setServerRunning( false );
IOUtil.closeQuiet( this._channel );
this._acceptor = null;
this._processors = null;
logger.log( Level.INFO, "EJOE server stopped." );
}
}
/**
* Reads the settings for this client from a given Properties instance
*
* @param props
*/
private void loadProperties( Properties props )
{
String clazz = null;
try
{
clazz = props.getProperty( "ejoe.serverHandler" );
this._serverInfo.setHandler( (ServerHandler) Class.forName( clazz ).newInstance() );
this._serverInfo.setInterface( props.getProperty( "ejoe.interface", "127.0.0.1" ) );
this._serverInfo.setPort( Integer.parseInt( props.getProperty( "ejoe.port", String
.valueOf( EJConstants.EJOE_PORT ) ) ) );
enableNonBlockingIO( Boolean.valueOf( props.getProperty( "ejoe.useNIO", "false" ) ).booleanValue() );
enableCompression( Boolean.valueOf( props.getProperty( "ejoe.compression", "false" ) ).booleanValue() );
enablePersistentConnections( Boolean.valueOf( props.getProperty( "ejoe.persistentConnection", "true" ) )
.booleanValue() );
enableHttpPackaging( Boolean.valueOf( props.getProperty( "ejoe.httpTunneling", "false" ) ).booleanValue() );
if ( Boolean.valueOf( props.getProperty( "ejoe.remoteClassloader", "false" ) ).booleanValue() )
{
enableRemoteClassLoading( true );
}
}
catch ( InstantiationException e )
{
logger.log( Level.SEVERE, "Can't instantiate class " + clazz );
throw new RuntimeException( e );
}
catch ( IllegalAccessException e )
{
logger.log( Level.SEVERE, "Can't access configured class " + clazz );
throw new RuntimeException( e );
}
catch ( ClassNotFoundException e )
{
logger.log( Level.SEVERE, "Can't locate configured class " + clazz );
throw new RuntimeException( e );
}
}
}