Package com.planet_ink.coffee_web.server

Source Code of com.planet_ink.coffee_web.server.WebServer

package com.planet_ink.coffee_web.server;
import java.util.*;
import java.util.concurrent.*;
import java.io.FileInputStream;
import java.io.IOException;
import java.io.InputStream;
import java.net.*;
import java.nio.channels.*;

import javax.net.ssl.SSLContext;

import com.planet_ink.coffee_web.interfaces.HTTPIOHandler;
import com.planet_ink.coffee_web.interfaces.MimeConverterManager;
import com.planet_ink.coffee_web.http.HTTPHeader;
import com.planet_ink.coffee_web.http.HTTPReader;
import com.planet_ink.coffee_web.http.HTTPReqProcessor;
import com.planet_ink.coffee_web.http.HTTPSReader;
import com.planet_ink.coffee_web.http.FileCache;
import com.planet_ink.coffee_web.http.MimeConverter;
import com.planet_ink.coffee_web.http.ServletManager;
import com.planet_ink.coffee_web.http.SessionManager;
import com.planet_ink.coffee_common.logging.Log;
import com.planet_ink.coffee_web.util.RunWrap;
import com.planet_ink.coffee_web.util.CWThread;
import com.planet_ink.coffee_web.util.CWThreadExecutor;
import com.planet_ink.coffee_web.util.CWConfig;

/*
Copyright 2012-2014 Bo Zimmerman

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.
*/

/**
* Both the main() kickoff for the coffeewebserver AND the main thread class for the same.
* This class handles all the socket listening and request management/timeout.
* When requests are received, they are passed off to an HTTPReader for processing,
* though a separate thread may timeout those readers at its discretion and shut
* them down.
* @author Bo Zimmerman
*
*/
public class WebServer extends Thread
{
  public static final  String    NAME        = "CoffeeWebServer";
  public static final String    POMVERSION    = "2.2";
  public static     double    VERSION;
  static { try { VERSION=Double.parseDouble(POMVERSION); } catch(final Exception e){ VERSION=0.0;} }
 
  private volatile boolean      shutdownRequested  = false;// notice of external shutdown request
  private Selector          servSelector     = null; // server io selector
  private final CWThreadExecutor   executor;          // request handler thread pool
  private Thread            timeoutThread    = null; // thread to timeout connected but idle channels
  private final CWConfig      config;             // list of all standard http ports to listen on
  private SSLContext           sslContext;
  private final String      serverName;           // a friendly name for this server instance
  private final LinkedList<HTTPIOHandler>      handlers;  // list of connected channels.. managed by timeoutthread
  private final Map<ServerSocketChannel, Boolean>  servChannels; // map of socket channels to an SSL boolean
  private final LinkedList<Runnable>        registerOps;
 
  public WebServer(String serverName, CWConfig config)
  {
    super("cweb-"+serverName);
    this.config=config;
    this.serverName=serverName;
   
    // setup the thread pool
    handlers = new LinkedList<HTTPIOHandler>();
    executor = new CWThreadExecutor(serverName,
                    config,
                    config.getCoreThreadPoolSize(), config.getMaxThreadPoolSize(), config.getMaxThreadIdleMs(),
                    TimeUnit.MILLISECONDS, config.getMaxThreadTimeoutSecs(), config.getMaxThreadQueueSize());

    servChannels = new HashMap<ServerSocketChannel, Boolean>();
    registerOps = new LinkedList<Runnable>();
   
    setDaemon(true);
   
    // if we are going to be listening on ssl, generate a global ssl context to use
    if((config.getHttpsListenPorts()==null)
    ||(config.getHttpsListenPorts().length==0))
      sslContext=null;
    else
      sslContext=HTTPSReader.generateNewContext(config);
  }

  /**
   * Returns the version of this web server
   * @return the version
   */
  public final String getVersion()
  {
    return Double.toString(VERSION);
  }
 
  /**
   * Return list of threads that have timed out according to server settings.
   * @return a collection of runnables
   */
  public List<Runnable> getOverdueThreads()
  {
    final Collection<RunWrap> wraps=executor.getTimeoutOutRuns(Integer.MAX_VALUE);
    final Vector<Runnable> overDueV=new Vector<Runnable>(wraps.size());
    for(final RunWrap wrap : wraps)
      overDueV.add(wrap.getRunnable());
    return overDueV;
  }

  /**
   * Open a single web server listening sockets and
   * register a selector for accepting connections.
   * @param listenPort the port to listen on
   * @throws IOException
   */
  private void openChannel(int listenPort, Boolean isSSL) throws IOException
  {
    final ServerSocketChannel servChan = ServerSocketChannel.open();
    final ServerSocket serverSocket = servChan.socket();
    serverSocket.bind (new InetSocketAddress (listenPort));
    servChan.configureBlocking (false);
    servChan.register (servSelector, SelectionKey.OP_ACCEPT);
    servChannels.put(servChan, isSSL);
    config.getLogger().info("Started "+(isSSL.booleanValue()?"ssl ":"http ")+serverName+" on port "+listenPort);
  }
 
  /**
   * Open the main web server listening sockets and
   * register a selector for accepting connections.
   * @throws IOException
   */
  private void openChannels() throws IOException
  {
    servSelector = Selector.open();
    boolean portOpen=false;
    IOException lastException=null;
   
    for(final int listenPort : config.getHttpListenPorts())
    {
      try
      {
        openChannel(listenPort, Boolean.FALSE);
        portOpen=true;
      }
      catch(final IOException e)
      {
        lastException=e;
        config.getLogger().severe("Port "+listenPort+": "+e.getMessage());
      }
    }
    if(sslContext != null)
    {
      for(final int listenPort : config.getHttpsListenPorts())
      {
        try
        {
          openChannel(listenPort, Boolean.TRUE);
          portOpen=true;
        }
        catch(final IOException e)
        {
          lastException=e;
          config.getLogger().severe("Port "+listenPort+": "+e.getMessage());
        }
      }
    }
    if((!portOpen)&&(lastException!=null))
      throw lastException;
  }

  /**
   * Initialize the timeoutthread.  It wakes up every second to look for timed out connections
   * It does this by calling a local final method to scan the open connections.
   * TODO: an executor thread pool that allows both scheduled and timeoutable entries would
   *      have allowed us to run this process on the pool.  Since that's not supported atm...
   */
  private void startTimeoutThread()
  {
    timeoutThread=new Thread(Thread.currentThread().getThreadGroup(),getName()+"Timeout")
    {
      @Override
      public void run()
      {
        try
        {
          config.getLogger().finer("Timeout Thread started");
          while(!shutdownRequested)
          {
            Thread.sleep(1000);
            timeOutStrayHandlers();
            config.getSessions().cleanUpSessions();
          }
        }
        catch(final InterruptedException e) {}
        finally
        {
          config.getLogger().info( "Timeout Thread shutdown");
        }
      }
    };
    timeoutThread.start();
  }
 
  /**
   * Handles a particular channel event from its given selectionkey.
   * So far, only accepted connections and readable keys are managed here.
   * @param key the channel event key
   * @throws IOException
   */
  private void handleSelectionKey(final SelectionKey key) throws IOException
  {
    if (key.isAcceptable()) // a connection was made, so add the handler
    {
      final ServerSocketChannel server = (ServerSocketChannel) key.channel();
      final SocketChannel channel = server.accept();
      if (channel != null)
      {
        HTTPIOHandler handler;
        if(servChannels.get(server).booleanValue())
          handler=new HTTPSReader(this, channel, sslContext);
        else
          handler=new HTTPReader(this, channel);
        channel.configureBlocking (false);
        channel.register (servSelector, SelectionKey.OP_READ, handler);
        synchronized(handlers) // synched because you can't iterate and modify, and because its a linkedlist
        {
          handlers.add(handler);
        }
      }
    }
    if(key.isReadable() // bytes were received on one of the channel .. so read!
    || (((key.interestOps() & SelectionKey.OP_WRITE)==SelectionKey.OP_WRITE) && key.isWritable()))
    {
      final HTTPIOHandler handler = (HTTPIOHandler)key.attachment();
      //config.getLogger().finer("Read/Write: "+handler.getName());
      if(!handler.isCloseable())
      {
        key.interestOps(key.interestOps() & ~SelectionKey.OP_WRITE);
        executor.execute(handler);
      }
    }
    else
    if(key.attachment() instanceof HTTPIOHandler)
    {
      final HTTPIOHandler handler = (HTTPIOHandler)key.attachment();
      config.getLogger().finer("Rejected handler key for "+handler.getName());
    }
  }
 
  /**
   * Scan the list of active channel connections for any that are timed out,
   * or otherwise need to be removed from this list.  If found, do so, and
   * queue up for actual closing (if necc).  After the handlers are all
   * scanned, then close any that need closing.
   */
  private final void timeOutStrayHandlers()
  {
    List<HTTPIOHandler> handlersToShutDown = null;
    synchronized(handlers)
    {
      // remove any stray handlers from time to time
      for(final Iterator<HTTPIOHandler> i = handlers.iterator(); i.hasNext(); )
      {
        final HTTPIOHandler handler=i.next();
        if(handler.isCloseable())
        {
          if(handlersToShutDown == null)
          {
            handlersToShutDown = new LinkedList<HTTPIOHandler>();
          }
          handlersToShutDown.add(handler);
          i.remove();
        }
      }
    }
    if(handlersToShutDown != null)
    {
      for(final HTTPIOHandler handler : handlersToShutDown)
      {
        handler.closeAndWait();
      }
    }
  }

  /**
   * The main web server loop
   * It blocks on its selector waiting for either accepted connections,
   * or data to be read, which it then farms out to another thread.
   * This is repeats until something external requests it to shut down.
   */
  @Override
  public void run()
  {
    try
    {
      openChannels(); // open the socket channel
      startTimeoutThread(); // start the channel timeout thread
    }
    catch(final IOException e)
    {
      config.getLogger().throwing("", "", e); // this is also fatal
      close();
      return;
    }
    while (!shutdownRequested)
    {
      try
      {
        final int n = servSelector.select();
        synchronized(registerOps)
        {
          while(!registerOps.isEmpty())
          {
            final Runnable registerOp=registerOps.removeFirst();
            registerOp.run();
          }
        }
        if (n == 0)
        {
          continue;
        }

        final Iterator<SelectionKey> it = servSelector.selectedKeys().iterator();
        while (it.hasNext())
        {
          final SelectionKey key = it.next();
          try
          {
            handleSelectionKey(key);
          }
          finally
          {
            it.remove();
          }
        }
      }
      catch(final CancelledKeyException t)
      {
        // ignore
      }
      catch(final IOException e)
      {
        config.getLogger().severe(e.getMessage());
      }
      catch(final Exception e)
      {
        config.getLogger().throwing("","",e);
      }
    }
    close();
    config.getLogger().info("Shutdown complete");
  }

  /**
   * Called either internally, or can be called externally to shutdown this
   * server instance.  Closes all the channels and cleans up any stray
   * activity.
   */
  public void close()
  {
   
    shutdownRequested=true;
    executor.shutdown();
    try
    {
      servSelector.close();
    }catch(final Exception e){} // ugh, why can't there be an "i don't care" exception syntax in java
    for(final ServerSocketChannel servChan : servChannels.keySet())
    {
      try
      {
        servChan.close();
      }catch(final Exception e){}
    }
    if(!executor.isShutdown())
    {
      try
      {
        executor.awaitTermination(10, TimeUnit.SECONDS);
      }
      catch (final InterruptedException e)
      {
        executor.shutdownNow();
      }
    }
    if(timeoutThread != null)
    {
      timeoutThread.interrupt();
    }
    synchronized(handlers)
    {
      for(final HTTPIOHandler handler : handlers)
      {
        handler.closeAndWait();
      }
      handlers.clear();
    }
  }
 
  /**
   * Enqueue a new socket channel to be registered for read notifications.
   * Does not do the action at once, but will, soon.
   * @param chan the socket channel to register
   * @param handler the handler to handle it.
   */
  public void registerNewHandler(final SocketChannel channel, final HTTPIOHandler handler)
  {
    synchronized(this.registerOps)
    {
      final Selector servSelector=this.servSelector;
      this.registerOps.add(new Runnable()
      {
        @Override
        public void run()
        {
          try
          {
            channel.configureBlocking (false);
            channel.register (servSelector, SelectionKey.OP_READ, handler);
            synchronized(handlers) // synched because you can't iterate and modify, and because its a linkedlist
            {
              handlers.add(handler);
            }
          }
          catch (final Exception e)
          {
            config.getLogger().throwing("", "", e);
          }
        }
      });
      servSelector.wakeup();
    }
  }

 
  /**
   * Enqueue a new socket channel to be registered for read notifications.
   * Does not do the action at once, but will, soon.
   * @param chan the socket channel to register
   * @param newOp the new operations for this channel
   */
  public void registerChannelInterest(final SocketChannel channel, final int newOp)
  {
    synchronized(this.registerOps)
    {
      final Selector servSelector=this.servSelector;
      this.registerOps.add(new Runnable()
      {
        @Override
        public void run()
        {
          final SelectionKey key = channel.keyFor(servSelector);
          if(key != null)
          {
            key.interestOps(newOp);
          }
        }
      });
      servSelector.wakeup();
    }
  }

  /**
   * Return the configuration for this web server instance
   * @return the config
   */
  public CWConfig getConfig()
  {
    return config;
  }
 
  /**
   * Create, Initialize, load, and create a web server configuration based around the given
   * ini filename and the given java logger.
   * @param log the java logger to use
   * @param iniInputStream the ini data to load further settings from
   * @return a populated configuration object to create a server from
   */
  public static CWConfig createConfig(java.util.logging.Logger log, InputStream iniInputStream) throws IOException
  {
    final CWConfig config=new CWConfig();
    return initConfig(config,log,iniInputStream);
  }
 
 
  /**
   * Initialize, load, and create a web server configuration based around the given
   * ini filename and the given java logger.
   * @param log the java logger to use
   * @param iniInputStream the ini data to load further settings from
   * @return a populated configuration object to create a server from
   */
  public static CWConfig initConfig(final CWConfig config, java.util.logging.Logger log, InputStream iniInputStream) throws IOException
  {
    config.setLogger(log);
    final Properties props=new Properties();
    props.load(iniInputStream);
   
    config.load(props);
   
    final ServletManager servletsManager = new ServletManager(config);
    final SessionManager sessionsManager = new SessionManager(config);
    final FileCache fileCacheManager = new FileCache(config,config.getFileManager());
    final MimeConverterManager mimeConverterManager = new MimeConverter(config);
    final HTTPReqProcessor fileGetter = new HTTPReqProcessor(config);
    config.setSessions(sessionsManager);
    config.setServletMan(servletsManager);
    config.setFileCache(fileCacheManager);
    config.setConverters(mimeConverterManager);
    config.setFileGetter(fileGetter);
   
    HTTPHeader.setKeepAliveHeader(HTTPHeader.KEEP_ALIVE.makeLine(
                    String.format(HTTPHeader.KEEP_ALIVE_FMT,
                    Integer.valueOf(config.getRequestMaxAliveSecs()),
                    Integer.valueOf(config.getRequestMaxPerConn()))));
    return config;
  }
 
  /**
   * Good olde main.  It does nothing but initialize logging, spawn a new web server
   * and then join its thread until it is gone.  I suppose I could just create the
   * web server and call run() on it, but somehow this feels better.
   * @param args As no external configuration is permitted, no args are accepted
   */
  public static void main(String[] args)
  {
   
    Log.instance().configureLogFile("web", 2);
    String debug="OFF";
    String iniFilename="coffeeweb.ini";
    for(final String arg : args)
    {
      if(arg.startsWith("BOOT="))
        iniFilename=arg.substring(5);
    }
   
    CWConfig config;
    try
    {
      config=WebServer.createConfig(Log.instance(), new FileInputStream(iniFilename));
    }
    catch (final Exception e)
    {
      e.printStackTrace();
      System.exit(-1);
      return; // an unhit operation, but my ide is argueing with me over it.
    }
   
    debug=config.getDebugFlag();
   
    for(final String arg : args)
    {
      if(arg.equalsIgnoreCase("DEBUG"))
      {
        debug="BOTH";
        config.setDebugFlag(debug);
      }
    }
    Log.instance().configureLog(Log.Type.info, "BOTH");
    Log.instance().configureLog(Log.Type.error, "BOTH");
    Log.instance().configureLog(Log.Type.warning, "BOTH");
    Log.instance().configureLog(Log.Type.debug, debug);
    Log.instance().configureLog(Log.Type.access, config.getAccessLogFlag());
    config.getLogger().info("Starting "+NAME+" "+VERSION);
   
    final WebServer server = new WebServer("server", config);
    config.setCoffeeWebServer(server);
    final Thread t = new CWThread(config, server, NAME);
    t.start();
    try
    {
      t.join();
    }
    catch(final InterruptedException e)
    {
      e.printStackTrace(System.err);
    }
  }
}
TOP

Related Classes of com.planet_ink.coffee_web.server.WebServer

TOP
Copyright © 2018 www.massapi.com. All rights reserved.
All source code are property of their respective owners. Java is a trademark of Sun Microsystems, Inc and owned by ORACLE Inc. Contact coftware#gmail.com.