All Downloads are FREE. Search and download functionalities are using the official Maven repository.

javaxt.http.Server Maven / Gradle / Ivy

package javaxt.http;

import java.io.IOException;
import java.net.HttpURLConnection;
import java.net.InetSocketAddress;
import java.net.URL;
import java.nio.channels.SelectionKey;
import java.nio.channels.Selector;
import java.nio.channels.ServerSocketChannel;
import java.nio.channels.SocketChannel;
import java.util.LinkedList;
import java.util.List;
import java.util.concurrent.atomic.AtomicBoolean;

import javax.servlet.ServletRequest;
import javax.servlet.ServletResponse;

import de.mhus.lib.core.MLog;
import de.mhus.lib.core.MThread;
import javaxt.http.servlet.HttpServlet;
import javaxt.http.servlet.HttpServletRequest;
import javaxt.http.servlet.HttpServletResponse;
import javaxt.http.servlet.ServletException;
import javaxt.http.servlet.WebSocketListener;

//******************************************************************************
//**  JavaXT Http Server
//******************************************************************************
/**
 * A lightweight, multi-threaded web server used to process HTTP requests and
 * send responses back to the client.
 *
 * The server requires an implementation of the HttpServlet class. As new
 * requests come in, they are passed to the HttpServlet.processRequest() method
 * which is used to generate a response.
 *
 ******************************************************************************/

public class Server extends MLog {

	private int numThreads;
	private InetSocketAddress[] addresses;
	private HttpServlet servlet;
	private List threads = new LinkedList<>();

	/** Maximum time that socket connections can remain idle. */
	private int maxIdleTime = 2 * 60000; // 2 minutes

	private boolean running = true;

	private List requestProcessorConnections = new LinkedList();

	private void addRequestProcessor(SocketConnection connection) {
		synchronized (requestProcessorConnections) {
			requestProcessorConnections.add(connection);
			requestProcessorConnections.notifyAll();
		}
	}

	private List socketMonitorConnections = new LinkedList();
	private boolean allowKeepAlive = false;
	public List sockets = new LinkedList<>();

	private void addMonitorSocket(SocketConnection connection) {
		synchronized (socketMonitorConnections) {
			socketMonitorConnections.add(connection);
			socketMonitorConnections.notifyAll();
		}
	}

	@SuppressWarnings("deprecation")
	public void stop() {
		running = false;
		log().i("Close JavaXT Server");
		synchronized (socketMonitorConnections) {
			socketMonitorConnections.notifyAll();
			for (SocketConnection con : socketMonitorConnections)
				if (con.isOpen())
					try {
						con.close();
					} catch (IOException e) {
						// TODO Auto-generated catch block
						e.printStackTrace();
					}
		}
		synchronized (requestProcessorConnections) {
			requestProcessorConnections.notifyAll();
			for (SocketConnection con : requestProcessorConnections)
				if (con.isOpen())
					try {
						con.close();
					} catch (IOException e) {
						// TODO Auto-generated catch block
						e.printStackTrace();
					}
		}
		MThread.sleep(500);
		for (SocketListener socket : sockets) {
			String url = "http://" + socket.address.getHostName() + ":" + socket.address.getPort();
			log().i("Send exit connect to " + url);
			try {
				HttpURLConnection huc = (HttpURLConnection) (new URL(url)).openConnection();
				HttpURLConnection.setFollowRedirects(false);
				huc.setConnectTimeout(15 * 1000);
				huc.setRequestMethod("GET");
				huc.connect();
				huc.disconnect();
			} catch (Throwable t) {
			}
		}
		sockets.clear();
		MThread.sleep(500);
		for (Thread thread : threads)
			if (thread.isAlive())
				thread.stop();
	}

	// **************************************************************************
	// ** Constructor
	// **************************************************************************
	/**
	 * Used to instantiate the Server on a given port.
	 */
	public Server(int port, int numThreads, HttpServlet servlet) {
		this(new InetSocketAddress(port), numThreads, servlet);
	}

	// **************************************************************************
	// ** Constructor
	// **************************************************************************
	/**
	 * Used to instantiate the Server on a given port and IP address.
	 */
	public Server(InetSocketAddress address, int numThreads, HttpServlet servlet) {
		this(new InetSocketAddress[] { address }, numThreads, servlet);
	}

	// **************************************************************************
	// ** Constructor
	// **************************************************************************
	/**
	 * Used to instantiate the Server on multiple ports and/or IP addresses.
	 */
	public Server(InetSocketAddress[] addresses, int numThreads, HttpServlet servlet) {
		this.addresses = addresses;
		this.numThreads = numThreads;
		this.servlet = servlet;
	}

	// **************************************************************************
	// ** Constructor
	// **************************************************************************
	/**
	 * Used to instantiate the Server on multiple ports and/or IP addresses.
	 */
	public Server(List addresses, int numThreads, HttpServlet servlet) {
		this(addresses.toArray(new InetSocketAddress[addresses.size()]), numThreads, servlet);
	}

	// **************************************************************************
	// ** Main
	// **************************************************************************
	/**
	 * Entry point for the application. Accepts command line arguments to
	 * specify which port to use and the maximum number of concurrent threads.
	 *
	 * @param args
	 *            Command line arguments. Options include:
	 *            
    *
  • -p to specify which port(s) to run on
  • *
  • -debug to specify whether to output debug messages to the * standard output stream.
  • *
  • -dir to specify a path to a directory where html, js, css, * images are found. The server will server content from this * directory to web clients.
  • *
*/ // public static void main(String[] args) throws Exception { // // // //Set local variables // java.io.File dir = null; // InetSocketAddress[] addresses = null; // // // //Parse inputs // if (args.length>0){ // // if (args.length==1){ // addresses = getAddresses(args[0]); // } // else{ // // for (int i=0; i addresses = new java.util.ArrayList(); for (String s : str.split(",")) { try { int port = Integer.parseInt(s); if (port < 0 || port > 65535) throw new Exception(); addresses.add(new InetSocketAddress(port)); } catch (Exception e) { throw new IllegalArgumentException(); } } return addresses.toArray(new InetSocketAddress[addresses.size()]); } // ************************************************************************** // ** Run // ************************************************************************** /** * Used to start the web server. Creates a thread pool and instantiates a * socket listener for each specified port/address. */ public void start() { // Create Thread Pool for (int i = 0; i < numThreads; i++) { addThread(new Thread(new RequestProcessor())).start(); } // Set up timer task to shutdown idle connections java.util.Timer timer = new java.util.Timer(); timer.scheduleAtFixedRate(new SocketMonitor(), maxIdleTime, maxIdleTime); // Create a new SocketListener for each port/address for (InetSocketAddress address : addresses) { addThread(new Thread(new SocketListener(address))).start(); } // Call init if (servlet != null) try { servlet.init(null); } catch (Exception e) { } } private Thread addThread(Thread thread) { threads.add(thread); return thread; } // ************************************************************************** // ** SocketListener // ************************************************************************** /** * Thread used to open a socket and accept client connections. Inbound * requests (client connections) are added to a queue and processed by the * next available RequestProcessor thread. Idle connections are * automatically closed after 5 minutes. */ private class SocketListener implements Runnable { private InetSocketAddress address; public SocketListener(InetSocketAddress address) { this.address = address; } @Override public void run() { sockets.add(this); String hostName = address.getHostName(); if (hostName.equals("0.0.0.0") || hostName.equals("127.0.0.1")) hostName = "localhost"; hostName += ":" + address.getPort(); System.out.print("Accepting connections on " + hostName + "\r\n"); Selector selector; ServerSocketChannel server = null; try { // Create the selector selector = Selector.open(); // Create a non-Blocking Server Socket Channel server = ServerSocketChannel.open(); server.configureBlocking(false); server.socket().bind(address); server.register(selector, SelectionKey.OP_ACCEPT); } catch (java.io.IOException e) { log().d("Failed to create listener for", hostName, e); // e.printStackTrace(); return; } // Pass Inbound Request to the RequestProcessor while (running) { SelectionKey key = null; try { if (selector.select() == 0) continue; java.util.Set keys = selector.selectedKeys(); java.util.Iterator it = keys.iterator(); // Process keys while (it.hasNext()) { // Get the selection key key = it.next(); // Remove it from the list to indicate that it is being // processed it.remove(); // Check whether the key is valid if (!key.isValid()) { continue; } // Process new connections to the server if (key.isAcceptable()) { // Accept the connection ServerSocketChannel s = (ServerSocketChannel) key.channel(); SocketChannel client = s.accept(); client.configureBlocking(false); SocketConnection connection = new SocketConnection(client, selector); // Add the new connection to the list of active // connections addMonitorSocket(connection); // Register for read events client.register(selector, SelectionKey.OP_READ, connection); continue; } // Process read key if (key.isReadable()) { SocketConnection connection = (SocketConnection) key.attachment(); synchronized (connection) { if (connection.isIdle.get()) { connection.isIdle.set(false); addRequestProcessor(connection); } else { connection.onReadable(); } } continue; } // Process write key if (key.isWritable()) { SocketConnection connection = (SocketConnection) key.attachment(); connection.onWritable(); } } } catch (Throwable e) { log().w(e); // Close the connection try { SocketConnection connection = (SocketConnection) key.attachment(); connection.close(); } catch (Exception ex) { } // In the rare event that channel didn't close via the // SocketConnection (e.g. NPE), close the socket channel // using // the key. try { key.channel().close(); } catch (Exception ex) { } } } log().i("Close " + hostName + "\r\n"); try { if (server != null) server.close(); } catch (IOException e) { // TODO Auto-generated catch block e.printStackTrace(); } } } // ************************************************************************** // ** RequestProcessor // ************************************************************************** /** * Thread used to process HTTP Requests and send responses back to the * client. As new HTTP requests come in they are added to a queue. Requests * in the queue are processed by instances of this class via the run method. */ private class RequestProcessor implements Runnable { public RequestProcessor() { } @Override public void run() { while (running) { // Wait for new requests to be added to the pool SocketConnection connection; synchronized (requestProcessorConnections) { while (requestProcessorConnections.isEmpty()) { try { requestProcessorConnections.wait(); } catch (InterruptedException e) { return; } } connection = requestProcessorConnections.remove(0); } // Process request and send a response back to the client HttpServletRequest request = null; HttpServletResponse response = null; try { if (servlet != null) { request = new HttpServletRequest(connection, servlet); response = new HttpServletResponse(request, connection); connection.onWritable(); servlet.service(request, response); } } catch (ServletException e) { log().w(e); if (request != null) { response = new HttpServletResponse(request, connection); response.setStatus(e.getStatusCode(), e.getMessage()); } else { // TODO: Need to propgate error to the client! } } catch (java.lang.OutOfMemoryError e) { log().e(e.toString()); return; } catch (Throwable e) { log().d(e); } // Flush the response if (response != null) { response.flushBuffer(); response.closeBuffer(); } // Check whether the channel is registered for write events. If // so // notify the channel that we are no longer interested in write // events. Otherwise the selector will loop indefinately. if (connection.opWrite) { try { connection.socketChannel.register(connection.selector, SelectionKey.OP_READ, connection); } catch (Exception e) { } } // Close the socket connection (as needed) boolean isKeepAlive = (request != null ? request.isKeepAlive() : false); if (!isAllowKeepAlive()) isKeepAlive = false; // TODO currently not working without // closeing the connection... if (!isKeepAlive) { try { connection.close(); } catch (java.io.IOException e) { } } // Mark the connection as inactive synchronized (connection) { connection.isIdle.set(true); } // Destroy the request and response objects if (request != null) { request.clear(); request = null; } if (response != null) { response.reset(); response = null; } } } } // ************************************************************************** // ** SocketMonitor // ************************************************************************** /** * TimerTask used to find and close idle connections. */ private class SocketMonitor extends java.util.TimerTask { @Override public void run() { long currTime = System.currentTimeMillis(); // Find idle connections synchronized (socketMonitorConnections) { java.util.ListIterator it = socketMonitorConnections.listIterator(); while (it.hasNext()) { SocketConnection connection = it.next(); if (currTime - connection.lastEvent > maxIdleTime) { if (connection.isOpen()) { try { connection.close(); } catch (Exception e) { } } it.remove(); } } } } } // ************************************************************************** // ** SocketConnection // ************************************************************************** /** * Simple wrapper for a SocketChannel. Logs reads/writes for the * SocketMonitor and is used to associate an SSLEngine with the given * SocketChannel. */ public static class SocketConnection { private final long startTime; private Long lastEvent; private SocketChannel socketChannel; private Selector selector; private javax.net.ssl.SSLEngine sslEngine; private final String localhost; private final String localaddress; private final int localport; private final java.net.InetSocketAddress remoteSocketAddress; private final List listeners; private final List read = new LinkedList(); private final List write = new LinkedList(); private final AtomicBoolean isIdle = new AtomicBoolean(true); private boolean opWrite = false; private SocketConnection(SocketChannel socketChannel, Selector selector) { this.socketChannel = socketChannel; this.selector = selector; startTime = new java.util.Date().getTime(); lastEvent = startTime; java.net.Socket socket = socketChannel.socket(); localhost = socket.getLocalAddress().getCanonicalHostName(); localport = socket.getLocalPort(); localaddress = socket.getLocalAddress().getHostAddress(); remoteSocketAddress = (java.net.InetSocketAddress) socket.getRemoteSocketAddress(); listeners = new LinkedList(); } /** Returns true if the socketChannel is not null and is open. */ public boolean isOpen() { if (socketChannel != null) return socketChannel.isOpen(); return false; } /** * Called by the SocketListener to notify users that the socket is ready * for reading. */ private void onReadable() { synchronized (read) { long t = System.currentTimeMillis(); if (read.isEmpty()) read.add(t); else read.set(0, t); read.notify(); } synchronized (listeners) { for (Listener listener : listeners) { listener.onReadable(); } } } /** * Called by the SocketListener to notify users that the socket is ready * for writing. */ private void onWritable() { synchronized (write) { long t = System.currentTimeMillis(); if (write.isEmpty()) write.add(t); else write.set(0, t); write.notify(); } synchronized (listeners) { for (Listener listener : listeners) { listener.onWritable(); } } } /** * Called by the SocketListener to notify users that the socket is being * closed. */ private void onClose() { synchronized (listeners) { for (Listener listener : listeners) { listener.onClose(); } } } /** * Used to add a Listener to this socket connection. Example: * *
		 * connection.addListener(new Server.SocketConnection.Listener() {
		 * 	public void onReadable() {
		 * 		log("Read!");
		 * 	}
		 * });
		 * 
*/ public void addListener(Listener listener) { synchronized (listeners) { listeners.add(listener); listeners.notify(); } } /** SocketConnection Listener class */ public static class Listener { public void onReadable() { } public void onWritable() { } public void onClose() { } } /** Returns the client IP address. */ public java.net.InetSocketAddress getRemoteSocketAddress() { return remoteSocketAddress; } /** * Returns the host name of the Internet Protocol (IP) interface on * which the request was received. */ public String getLocalHost() { return localhost; } /** * Returns the Internet Protocol (IP) address of the interface on which * the request was received. */ public String getLocalAddress() { return localaddress; } /** * Returns the Internet Protocol (IP) port number of the interface on * which the request was received. */ public int getLocalPort() { return localport; } /** * Used to read data from the SocketChannel. The method will return * immediately after receiving bytes from the socket. Note that a * SocketChannel in non-blocking mode cannot read any more bytes than * are immediately available from the socket's input buffer. In this * case, this method will wait until the connection is readable. * Unfortunately, this is no way for this method to know whether the * client is done sending data so no checksum is performed to ensure * that all the bytes came across cleanly. */ public int read(java.nio.ByteBuffer buffer) throws java.io.IOException { if (!isOpen()) throw new java.io.IOException("SocketConnection is closed!"); int numBytesRead; synchronized (read) { numBytesRead = socketChannel.read(buffer); read.clear(); if (numBytesRead == 0) { while (read.isEmpty()) { try { read.wait(); } catch (InterruptedException e) { break; } } numBytesRead = socketChannel.read(buffer); read.clear(); } } if (numBytesRead == -1) { close(); throw new java.io.IOException("Received -1 bytes. Socket is closed."); } lastEvent = new java.util.Date().getTime(); return numBytesRead; } /** * Used to write data to the SocketChannel. Note that a SocketChannel in * non-blocking mode cannot write any more bytes than are free in the * socket's output buffer. In this case, the server will wait until the * connection becomes writable. Returns only after all of the bytes in * the buffer have been sent. * * @param length * Number of bytes in the buffer. This is used to ensure that * all the bytes were sent to the client. Note that buffer * length does not always equal buffer capacity. */ public int write(java.nio.ByteBuffer buffer, int length) throws java.io.IOException { if (!isOpen()) throw new java.io.IOException("SocketConnection is closed!"); int numBytesWrite = socketChannel.write(buffer); if (numBytesWrite == -1) throw new java.io.IOException("Socket is closed."); if (numBytesWrite < length) { synchronized (write) { write.clear(); write.notify(); } // Register for write events. Note that this will slow things // down // quite a bit. In my dev environment, performance drops from // 11k // requests per second to about 8k per second. Also, note that // we // need to unregister from OP_WRITE events once we are done // writing. // Otherwise, the SocketListener gets stuck in an infinite loop. if (!opWrite) { try { socketChannel.register(selector, SelectionKey.OP_READ | SelectionKey.OP_WRITE, this); opWrite = true; } catch (Exception e) { } } // Wait for write operations while (numBytesWrite < length) { int x; synchronized (write) { while (write.isEmpty()) { try { write.wait(); } catch (InterruptedException e) { break; } } x = socketChannel.write(buffer); write.clear(); write.notify(); } if (x == -1) throw new java.io.IOException("Socket is closed."); numBytesWrite += x; lastEvent = new java.util.Date().getTime(); } } else { synchronized (write) { write.clear(); write.notify(); } } return numBytesWrite; } /** Used to close the socketChannel and update any listeners. */ public void close() throws java.io.IOException { if (socketChannel != null) { socketChannel.close(); } socketChannel = null; sslEngine = null; onClose(); } public javax.net.ssl.SSLEngine getSSLEngine() { return sslEngine; } public void setSSLEngine(javax.net.ssl.SSLEngine sslEngine) { this.sslEngine = sslEngine; } public SocketChannel getChannel() { return socketChannel; } } // ************************************************************************** // ** ServletTest // ************************************************************************** /** * Simple implementation of an JavaXT HttpServlet. Simply returns the * request headers and body back to the client in plain text. */ private class ServletTest extends javaxt.http.servlet.HttpServlet { private final java.io.File dir; private final String s = System.getProperty("file.separator"); public ServletTest(java.io.File dir) throws Exception { this.dir = dir; java.io.File keystore = new java.io.File("/temp/keystore.jks"); if (keystore.exists()) { setKeyStore(keystore, "password"); setTrustStore(keystore, "password"); } } @Override public void service(ServletRequest req, ServletResponse res) throws javax.servlet.ServletException, IOException { // public void service(HttpServletRequest request, // HttpServletResponse response) throws ServletException, // java.io.IOException { HttpServletRequest request = (HttpServletRequest) req; HttpServletResponse response = (HttpServletResponse) res; // Print the requested URL log().d("New Request From", request.getRemoteAddr(), request.getMethod(), ": ", request.getURL()); if (request.isWebSocket()) { new WebSocketListener(request, response) { @Override public void onConnect() { send("Hello There!"); } @Override public void onText(String str) { // System.out.println(str); send("Message recieved at " + new java.util.Date()); } @Override public void onDisconnect(int statusCode, String reason, boolean remote) { // System.out.println("Goodbye..."); } }; return; } if (dir != null) { // Get requested path String path = request.getURL().getPath(); if (path.length() > 1 && path.startsWith("/")) path = path.substring(1); // Construct a physical file path using the url java.io.File file = new java.io.File(dir + s + path); if (file.exists()) { if (file.isDirectory()) { file = new java.io.File(file, "index.html"); } } // If the file doesn't exist, return an error if (!file.exists()) { response.setStatus(404); } else { // Dump the file content to the servlet output stream String ext = null; int x = file.getName().lastIndexOf("."); if (x != -1) ext = file.getName().substring(x + 1).toLowerCase(); response.write(file, getContentType(ext), true); } } else { // Send sample http response to the client try { byte[] header = request.toString().getBytes("UTF-8"); byte[] body = request.getBody(); byte[] msg = new byte[header.length + body.length]; System.arraycopy(header, 0, msg, 0, header.length); System.arraycopy(body, 0, msg, header.length, body.length); header = null; body = null; response.setContentType("text/plain"); response.write(msg); msg = null; } catch (Exception e) { } } log().t(request); log().t(response); } private String getContentType(String ext) { if (ext != null) { if (ext.equals("css")) return "text/css"; if (ext.equals("htm") || ext.equals("html")) return "text/html"; if (ext.equals("js")) return "text/javascript"; if (ext.equals("txt")) return "text/plain"; if (ext.equals("gif")) return "image/gif"; if (ext.equals("jpg")) return "image/jpeg"; if (ext.equals("png")) return "image/png"; if (ext.equals("ico")) return "image/vnd.microsoft.icon"; } return "application/octet-stream"; } } // ************************************************************************** // ** log // ************************************************************************** // /** Used to log messages to the standard output stream when the server is // * in debug mode. // */ // public static void log(Object obj) { // if (!debug) return; // // String md = "[" + getTime() + "] "; // String padding = ""; // for (int i=0; i0){ // String[] arr = str.split("\n"); // for (int i=0; i




© 2015 - 2025 Weber Informatics LLC | Privacy Policy