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
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
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 }