View Javadoc

1   /**********************************************************************
2    * EJServer.java
3    * created on 06.08.2004 by netseeker
4    * $Source: /cvsroot/ejoe/EJOE/src/de/netseeker/ejoe/EJServer.java,v $
5    * $Date: 2007/11/17 10:59:40 $
6    * $Revision: 1.78 $
7    *
8    * ====================================================================
9    *
10   *  Copyright 2005-2006 netseeker aka Michael Manske
11   *
12   *  Licensed under the Apache License, Version 2.0 (the "License");
13   *  you may not use this file except in compliance with the License.
14   *  You may obtain a copy of the License at
15   *
16   *      http://www.apache.org/licenses/LICENSE-2.0
17   *
18   *  Unless required by applicable law or agreed to in writing, software
19   *  distributed under the License is distributed on an "AS IS" BASIS,
20   *  WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
21   *  See the License for the specific language governing permissions and
22   *  limitations under the License.
23   * ====================================================================
24   *
25   * This file is part of the ejoe framework.
26   * For more information on the author, please see
27   * <http://www.manskes.de/>.
28   *
29   *********************************************************************/
30  package de.netseeker.ejoe;
31  
32  import java.io.FileInputStream;
33  import java.io.IOException;
34  import java.net.InetSocketAddress;
35  import java.nio.channels.ServerSocketChannel;
36  import java.util.Properties;
37  import java.util.logging.Level;
38  import java.util.logging.Logger;
39  import java.util.zip.Deflater;
40  
41  import de.netseeker.ejoe.core.ChannelRegistrar;
42  import de.netseeker.ejoe.core.CombinedConnectionProcessor;
43  import de.netseeker.ejoe.core.ConnectionAcceptor;
44  import de.netseeker.ejoe.core.EJServerRegistry;
45  import de.netseeker.ejoe.handler.ClassHandler;
46  import de.netseeker.ejoe.handler.PingHandler;
47  import de.netseeker.ejoe.handler.ServerHandler;
48  import de.netseeker.ejoe.handler.ServerHandlerMapping;
49  import de.netseeker.ejoe.io.IOUtil;
50  import de.netseeker.ejoe.jmx.EJServerConfig;
51  import de.netseeker.ejoe.jmx.EJServerConfigMBean;
52  import de.netseeker.ejoe.request.ClassloaderRequest;
53  import de.netseeker.ejoe.request.PingRequest;
54  
55  /***
56   * This is the server component of EJOE. EJOE is a request object broker in it's natural meaning. You have to use this
57   * component if you want retrieve and send data from/to EJOE clients. EJOE offers basically three things for you:
58   * <ol>
59   * <li>a multithreaded, high performance io server</li>
60   * <li>(de)serializing input of input objects send by clients and return objects provided by YOUR business logic</li>
61   * <li>a simple, clean and unique interface to integrate a object request broker into your application</li>
62   * </ol>
63   * <p>
64   * Basic usage:
65   * 
66   * <pre>
67   *                       EJServer server = new EJServer( new EchoHandler() );
68   *                       //to allow persistent client connections use
69   *                       //server.enablePersistentConnections( true );
70   *                       
71   *                       //to use old style blocking IO instead of NIO use
72   *                       //server.enableNonBlockingIO( false );
73   *                       
74   *                       server.start();
75   *                       ...
76   *                       server.stop();
77   * </pre>
78   * 
79   * </p>
80   * 
81   * @author netseeker aka Michael Manske
82   * @since 0.3.0
83   */
84  public class EJServer
85  {
86      private transient static final Logger logger         = Logger.getLogger( EJServer.class.getName() );
87  
88      private transient ConnectionAcceptor  _acceptor;
89  
90      private transient ChannelRegistrar[]  _processors;
91  
92      private transient ServerSocketChannel _channel;
93  
94      private ServerInfo          _serverInfo    = new ServerInfo();
95  
96      private boolean             _isShuttingDown = false;
97  
98      private EJServerConfigMBean _jmxConfigBean;
99  
100     /***
101      * Creates an instance of the EJOE server component pre-configured whith a default value for the used port
102      * 
103      * @param handler an implementation of de.netseeker.ejoe.ServerHandler
104      */
105     public EJServer(final ServerHandler handler)
106     {
107         this( handler, EJConstants.EJOE_PORT );
108     }
109 
110     /***
111      * Creates an instance of the EJOE server component pre-configured whith a default value for the used port
112      * 
113      * @param handler an implementation of de.netseeker.ejoe.ServerHandler
114      * @param bindAddr the IP address identifying the network interface to which EJServer should bind itself
115      */
116     public EJServer(final ServerHandler handler, String bindAddr)
117     {
118         this( handler, bindAddr, EJConstants.EJOE_PORT );
119     }
120 
121     /***
122      * Creates an instance of the EJOE server component
123      * 
124      * @param handler an implementation of de.netseeker.ejoe.ServerHandler
125      * @param port the port EJOE should listen to
126      */
127     public EJServer(final ServerHandler handler, int port)
128     {
129         ServerHandlerMapping mapping = null;
130         if ( handler instanceof ServerHandlerMapping )
131         {
132             mapping = (ServerHandlerMapping) handler;
133         }
134         else
135         {
136             mapping = new ServerHandlerMapping();
137             mapping.setDefaultHandler( handler );
138         }
139 
140         mapping.addHandlerMapping( PingRequest.UNIQUE_NAME, new PingHandler() );
141 
142         _serverInfo.setHandler( handler );
143         _serverInfo.setPort( port );
144     }
145 
146     /***
147      * Creates an instance of the EJOE server component
148      * 
149      * @param handler an implementation of de.netseeker.ejoe.ServerHandler
150      * @param bindAddr the IP address identifying the network interface to which EJServer should bind itself
151      * @param port the port EJOE should listen to
152      */
153     public EJServer(final ServerHandler handler, String bindAddr, int port)
154     {
155         this( handler, port );
156         _serverInfo.setInterface( bindAddr );
157     }
158 
159     /***
160      * Creates an instance of EJServer pre-configured with settings from the global ejserver.properties file. You MUST
161      * provide such an property file on the classpath to use this constructor.
162      */
163     public EJServer()
164     {
165         Properties props = new Properties();
166         try
167         {
168             props.load( EJServer.class.getResourceAsStream( "/ejserver.properties" ) );
169             loadProperties( props );
170         }
171         catch ( IOException e )
172         {
173             logger.log( Level.SEVERE, "ejserver.properties could not be read! Make sure you have placed"
174                     + " a valid properties file in your classpath.", e );
175             throw new RuntimeException( e );
176         }
177     }
178 
179     /***
180      * Creates an instance of EJServer pre-configured with settings from the given properties store
181      * 
182      * @param properties properties store containing EJServer settings
183      */
184     public EJServer(Properties properties)
185     {
186         loadProperties( properties );
187     }
188 
189     /***
190      * Creates an instance of EJServer pre-configured with settings from a global properties file.
191      * 
192      * @param pathToConfigFile path to the properties file
193      */
194     public EJServer(String pathToConfigFile)
195     {
196         Properties props = new Properties();
197         FileInputStream fis = null;
198         try
199         {
200             fis = new FileInputStream( pathToConfigFile );
201             props.load( fis );
202             loadProperties( props );
203         }
204         catch ( IOException e )
205         {
206             logger.log( Level.SEVERE, "ejserver.properties could not be read! Make sure you have placed"
207                     + " a valid properties file in your classpath.", e );
208             throw new RuntimeException( e );
209         }
210         finally
211         {
212             IOUtil.closeQuiet( fis );
213         }
214     }
215 
216     /***
217      * Indicates if this EJServer is running and ready to accept and process incoming connections
218      * 
219      * @return true if this EJServer is running and ready to accept and process incoming connections otherwise false
220      */
221     public boolean isRunning()
222     {
223         return (_processors != null && _acceptor != null && _acceptor.isAlive() && !_isShuttingDown);
224     }
225 
226     /***
227      * Returns the current amount of supported concurrent read processor threads.
228      * 
229      * @return
230      */
231     public int getMaxReadProcessors()
232     {
233         return _serverInfo.getMaxReadProcessors();
234     }
235 
236     /***
237      * Sets the amount of threads used for processing read operations on accepted connections. This will indirectly
238      * control the amount of concurrently processed io read operations and adapter calls.
239      * 
240      * @param maxProcessors new amount of threads used for processing accepted connections
241      * @see #setMaxWriteProcessors(int)
242      */
243     public void setMaxReadProcessors( int maxProcessors )
244     {
245         if ( !isRunning() )
246         {
247             _serverInfo.setMaxReadProcessors( maxProcessors );
248         }
249         else
250         {
251             throw new IllegalStateException( "The value for the maximum of used read processor"
252                     + "threads can't be changed while EJOE is already running!" );
253         }
254     }
255 
256     /***
257      * Returns the current amount of supported concurrent write processor threads.
258      * 
259      * @return
260      */
261     public int getMaxWriteProcessors()
262     {
263         return _serverInfo.getMaxWriteProcessors();
264     }
265 
266     /***
267      * Sets the amount of threads used for processing write operations. This will directly control the amount of
268      * concurrently processed io socket write operations.
269      * 
270      * @param maxProcessors new amount of threads used for processing io socket write operations
271      * @see #setMaxReadProcessors(int)
272      */
273     public void setMaxWriteProcessors( int maxProcessors )
274     {
275         if ( !isRunning() )
276         {
277             _serverInfo.setMaxWriteProcessors( maxProcessors );
278         }
279         else
280         {
281             throw new IllegalStateException(
282                                              "The value for the maximum of used write processor threads can't be changed "
283                                                      + "while EJOE is already running!" );
284         }
285     }
286 
287     /***
288      * Enables/Disables automic, periodic control of the used worker pools. If enabled the worker pools will be checked
289      * regulary and shrinked or increased if neccessary. The control ensures that the pool sizes will never extend the
290      * size of maxReadProcessors/maxWriteProcessors.
291      * 
292      * @param enable
293      * @see #enableThreadPoolResizeControl(boolean, long)
294      */
295     public void enableThreadPoolResizeControl( boolean enable )
296     {
297         if ( _processors == null )
298         {
299             _serverInfo.setAutomaticThreadPoolResize( enable );
300         }
301         else
302         {
303             throw new IllegalStateException( "ThreadPoolResizeControl can't be changed"
304                     + " while EJOE is already running!" );
305         }
306     }
307 
308     /***
309      * This method works exactly as {@link #enableThreadedProcessorUsage(boolean)} but allows setting of a custom time
310      * period between control checks of the used worker pools.
311      * 
312      * @param enable
313      * @param controlPeriod period between control checks in milliseconds
314      * @see #enableThreadPoolResizeControl(boolean)
315      */
316     public void enableThreadPoolResizeControl( boolean enable, long controlPeriod )
317     {
318         if ( _processors == null )
319         {
320             _serverInfo.setAutomaticThreadPoolResize( enable );
321             _serverInfo.setPoolResizePeriod( controlPeriod );
322         }
323         else
324         {
325             throw new IllegalStateException( "ThreadPoolResizeControl can't be changed"
326                     + " while EJOE is already running!" );
327         }
328     }
329 
330     /***
331      * Returns whether compression has been enabled or not.
332      * 
333      * @return
334      */
335     public boolean hasCommpression()
336     {
337         return _serverInfo.hasCompression();
338     }
339 
340     /***
341      * Enables or disables the usage of compressing/decompressing for outgoing/incoming data. Enabling compression *can*
342      * result in signifcantly better throughput especially when using a text based SerializeAdapter in confunction with
343      * large data objects.
344      * 
345      * @see de.netseeker.ejoe.adapter.SerializeAdapter
346      */
347     public void enableCompression( boolean enable )
348     {
349         this._serverInfo.setCompression( enable );
350     }
351 
352     /***
353      * Enables the usage of compressing/decompressing for outgoing/incoming data with the given compression level.
354      * Enabling compression *can* result in signifcantly better throughput especially when using a text based
355      * SerializeAdapter in confunction with large data objects.
356      * 
357      * @param compressionLevel the level of compression to use, must be in range of 0-9
358      * @see de.netseeker.ejoe.adapter.SerializeAdapter
359      */
360     public void enableCompression( int compressionLevel )
361     {
362         if ( compressionLevel < Deflater.NO_COMPRESSION || compressionLevel > Deflater.BEST_COMPRESSION )
363         {
364             throw new IllegalArgumentException( "Compressionlevel must be in the range of " + Deflater.NO_COMPRESSION
365                     + ':' + Deflater.BEST_COMPRESSION );
366         }
367         this._serverInfo.setCompression( true );
368         this._serverInfo.setCompressionLevel( compressionLevel );
369     }
370 
371     /***
372      * Returns whether NIO has been enabled or not.
373      * 
374      * @return
375      */
376     public boolean hasNonBlockingIO()
377     {
378         return this._serverInfo.hasNonBlockingReadWrite();
379     }
380 
381     /***
382      * Enables/disables new style non-blocking IO for read and write operations. Sometimes this can be significantly
383      * faster than using blocking io. Be aware that connection acception as well as EJOES internal connection
384      * handshaking will use NIO anyway to ensure high latency.
385      */
386     public void enableNonBlockingIO( boolean enable )
387     {
388         if ( enable || (!enable && !hasHttPackaging()) )
389         {
390             this._serverInfo.setNonBlockingReadWrite( enable );
391         }
392         else
393         {
394             throw new IllegalStateException(
395                                              "You have tried to disable non-blocking IO while HTTP support is still active! "
396                                                      + "HTTP support is only implemented for non-blocking IO. "
397                                                      + "To disable non-blocking IO you must disable HTTP packaging first." );
398         }
399     }
400 
401     /***
402      * Returns whether support for persistent connections has been enabled or not.
403      * 
404      * @return
405      */
406     public boolean hasPersistentConnections()
407     {
408         return this._serverInfo.isPersistent();
409     }
410 
411     /***
412      * Enables/disables support for persistents client connections. Usage of persistent connections is more performant
413      * in most cases because the client doesn't need to open a new connection on each request. The drawback of
414      * persistent connections can be a higher server load because the server may have to handle a lot of "idle" client
415      * connections.
416      * 
417      * @param enable
418      */
419     public void enablePersistentConnections( boolean enable )
420     {
421         this._serverInfo.setPersistent( enable );
422     }
423 
424     /***
425      * Enables/disables support for http packaging.
426      * 
427      * @param enable
428      */
429     public void enableHttpPackaging( boolean enable )
430     {
431         if ( (enable && hasNonBlockingIO()) || !enable )
432         {
433             this._serverInfo.setHttp( enable );
434         }
435         else
436         {
437             throw new IllegalStateException(
438                                              "HTTP support is only implemted for non-blocking IO! Enable non-blocking IO if you want to use HTTP packaging..." );
439         }
440     }
441 
442     /***
443      * Returns whether support for HTTP packaging has been enabled or not.
444      * 
445      * @return
446      */
447     public boolean hasHttPackaging()
448     {
449         return this._serverInfo.isHttp();
450     }
451 
452     /***
453      * Returns the number of Connection Processors used for delegating network IO operations to reader/writer workers
454      * 
455      * @return the number of used Connection Processors
456      */
457     public int getConnectionProcessors()
458     {
459         return this._serverInfo.getTargetedConnectionProcessors();
460     }
461 
462     /***
463      * Sets the number of Connection Processors to use for delegating network IO operations to reader/writer workers. It
464      * is strongly recommended to use the number of physically available processor units.
465      * 
466      * @param count the number of Connection Processors to use
467      */
468     public void setConnectionProcessors( int count )
469     {
470         if ( !isRunning() )
471         {
472             this._serverInfo.setTargetedConnectionProcessors( count );
473         }
474         else
475         {
476             throw new IllegalStateException( "The number of Connection Processors must not be changed "
477                     + "while EJServer is already running!" );
478         }
479     }
480 
481     /***
482      * Enables support for remote classloading
483      * 
484      * @param enable
485      */
486     public void enableRemoteClassLoading( boolean enable )
487     {
488         if ( !isRunning() )
489         {
490             _serverInfo.setClassServerEnabled( enable );
491             ServerHandlerMapping mapping = (ServerHandlerMapping) _serverInfo.getHandler();
492 
493             if ( enable )
494             {
495                 mapping.addHandlerMapping( ClassloaderRequest.UNIQUE_NAME, new ClassHandler() );
496             }
497             else
498             {
499                 mapping.removeHandlerMapping( ClassloaderRequest.UNIQUE_NAME );
500             }
501 
502         }
503         else
504         {
505             throw new IllegalStateException( "Remote classloading can't be changed" + " while EJOE is already running!" );
506         }
507     }
508 
509     /***
510      * Returns a JMX compliant bean to configure EJServer via JMX
511      * 
512      * @return
513      */
514     public EJServerConfigMBean getJMXConfigurationBean()
515     {
516         if ( this._jmxConfigBean == null )
517         {
518             this._jmxConfigBean = new EJServerConfig( this );
519         }
520 
521         return this._jmxConfigBean;
522     }
523 
524     /***
525      * @return
526      */
527     public IServerInfo getServerInfo()
528     {
529         return this._serverInfo;
530     }
531 
532     /***
533      * (Re)Starts the main server as well as the class loader server (if it's configured)
534      */
535     public void start() throws IOException
536     {
537         logger.log( Level.INFO, "Starting EJOE server..." );
538 
539         if ( _serverInfo.isServerRunning() )
540         {
541             logger.log( Level.WARNING, "EJOE server already running - will try a restart." );
542             stop();
543         }
544 
545         _isShuttingDown = false;
546         ConnectionAcceptor acceptor = null;
547         ChannelRegistrar processors[] = new ChannelRegistrar[this._serverInfo.getTargetedConnectionProcessors()];
548         ServerSocketChannel channel = null;
549 
550         try
551         {
552             channel = ServerSocketChannel.open();
553             channel.configureBlocking( false );
554             channel.socket().setReuseAddress( true );
555             InetSocketAddress address = new InetSocketAddress( _serverInfo.getInterface(), _serverInfo.getPort() );
556             channel.socket().bind( address, 1024 );
557             this._serverInfo.setHost( address.getHostName() + ':' + address.getPort() );
558             // this._connectionHeader.setChannel(channel);
559             for ( int i = 0; i < processors.length; i++ )
560             {
561                 processors[i] = new CombinedConnectionProcessor( this._serverInfo );
562                 ((Thread) processors[i]).setDaemon( true );
563             }
564 
565             acceptor = new ConnectionAcceptor( channel, processors );
566             //acceptor.setDaemon( true );
567         }
568         catch ( IOException e )
569         {
570             logger.log( Level.SEVERE, "!!! IOException occured !!! ", e );
571             IOUtil.closeQuiet( channel );
572             throw (e);
573         }
574 
575         for ( int i = 0; i < processors.length; i++ )
576         {
577             ((CombinedConnectionProcessor) processors[i]).start();
578         }
579         acceptor.start();
580 
581         this._serverInfo.setServerRunning( true );
582         this._processors = processors;
583         this._acceptor = acceptor;
584         this._channel = channel;
585 
586         EJServerRegistry.getInstance().register( this );
587 
588         if ( logger.isLoggable( Level.INFO ) )
589         {
590             logger.log( Level.INFO, "EJOE server listening on: " + channel.socket().getLocalSocketAddress() );
591             logger.log( Level.INFO, "Using " + processors.length + " Connection Processor"
592                     + (processors.length > 1 ? "s" : "") );
593             logger.log( Level.INFO, "Using non-blocking IO: " + this._serverInfo.hasNonBlockingReadWrite() );
594             logger.log( Level.INFO, "Allowing persistent client connections: " + this._serverInfo.isPersistent() );
595             logger.log( Level.INFO, "Using compression: " + this._serverInfo.hasCompression() );
596             logger.log( Level.INFO, "Supporting HTTP tunneling: " + this._serverInfo.isHttp() );
597             logger.log( Level.INFO, "Supporting remote classloading: " + this._serverInfo.isClassServerEnabled() );
598             logger.log( Level.INFO, "Using automatic thread pool resizing: "
599                     + this._serverInfo.isAutomaticThreadPoolResize() );
600             logger.log( Level.INFO, "EJOE server started successfully." );
601         }
602     }
603 
604     /***
605      * Stops the main server as well as the class loader server (if it's running)
606      */
607     public void stop()
608     {
609         logger.log( Level.INFO, "Stopping EJOE server..." );
610         _isShuttingDown = true;
611         EJServerRegistry.getInstance().deRegister( this );
612 
613         if ( isRunning() )
614         {
615             this._acceptor.interrupt();
616             for ( int i = 0; i < this._processors.length; i++ )
617             {
618                 ((Thread) this._processors[i]).interrupt();
619             }
620             this._serverInfo.setServerRunning( false );
621             IOUtil.closeQuiet( this._channel );
622             this._acceptor = null;
623             this._processors = null;
624             logger.log( Level.INFO, "EJOE server stopped." );
625         }
626     }
627 
628     /***
629      * Reads the settings for this client from a given Properties instance
630      * 
631      * @param props
632      */
633     private void loadProperties( Properties props )
634     {
635         String clazz = null;
636 
637         try
638         {
639             clazz = props.getProperty( "ejoe.serverHandler" );
640             this._serverInfo.setHandler( (ServerHandler) Class.forName( clazz ).newInstance() );
641 
642             this._serverInfo.setInterface( props.getProperty( "ejoe.interface", "127.0.0.1" ) );
643             this._serverInfo.setPort( Integer.parseInt( props.getProperty( "ejoe.port", String
644                     .valueOf( EJConstants.EJOE_PORT ) ) ) );
645 
646             enableNonBlockingIO( Boolean.valueOf( props.getProperty( "ejoe.useNIO", "false" ) ).booleanValue() );
647 
648             enableCompression( Boolean.valueOf( props.getProperty( "ejoe.compression", "false" ) ).booleanValue() );
649 
650             enablePersistentConnections( Boolean.valueOf( props.getProperty( "ejoe.persistentConnection", "true" ) )
651                     .booleanValue() );
652 
653             enableHttpPackaging( Boolean.valueOf( props.getProperty( "ejoe.httpTunneling", "false" ) ).booleanValue() );
654 
655             if ( Boolean.valueOf( props.getProperty( "ejoe.remoteClassloader", "false" ) ).booleanValue() )
656             {
657                 enableRemoteClassLoading( true );
658             }
659         }
660         catch ( InstantiationException e )
661         {
662             logger.log( Level.SEVERE, "Can't instantiate class " + clazz );
663             throw new RuntimeException( e );
664         }
665         catch ( IllegalAccessException e )
666         {
667             logger.log( Level.SEVERE, "Can't access configured class " + clazz );
668             throw new RuntimeException( e );
669         }
670         catch ( ClassNotFoundException e )
671         {
672             logger.log( Level.SEVERE, "Can't locate configured class " + clazz );
673             throw new RuntimeException( e );
674         }
675     }
676 }