Skip to content

Commit

Permalink
added httpserver
Browse files Browse the repository at this point in the history
  • Loading branch information
n.plaschke committed Oct 16, 2016
0 parents commit 0e403f7
Show file tree
Hide file tree
Showing 37 changed files with 2,554 additions and 0 deletions.
29 changes: 29 additions & 0 deletions build.gradle
Original file line number Diff line number Diff line change
@@ -0,0 +1,29 @@
group 'io.github.chumper'
version '1.0-SNAPSHOT'

apply plugin: 'java'

sourceCompatibility = 1.8

repositories {
mavenCentral()
}

dependencies {
testCompile group: 'junit', name: 'junit', version: '4.11'
testCompile group: 'org.apache.httpcomponents', name: 'httpclient', version: '4.5.2'
testCompile group: 'org.apache.httpcomponents', name: 'fluent-hc', version: '4.5.2'

compile "com.typesafe:config:1.3.1"
compile "org.mongodb:mongo-java-driver:3.3.0"
}

//create a single Jar with all dependencies
task fatJar(type: Jar) {
manifest {
attributes 'Main-Class': 'io.github.chumper.webserver.Main'
}
baseName = project.name + '-all'
from { configurations.compile.collect { it.isDirectory() ? it : zipTree(it) } }
with jar
}
80 changes: 80 additions & 0 deletions src/main/java/io/github/chumper/webserver/Main.java
Original file line number Diff line number Diff line change
@@ -0,0 +1,80 @@
package io.github.chumper.webserver;

import com.typesafe.config.Config;
import com.typesafe.config.ConfigFactory;

import java.io.BufferedReader;
import java.io.IOException;
import java.io.InputStreamReader;
import java.util.concurrent.ExecutionException;

import io.github.chumper.webserver.core.HttpServer;
import io.github.chumper.webserver.data.MongoDbCommentRepository;
import io.github.chumper.webserver.handler.HttpCommentHandler;
import io.github.chumper.webserver.handler.HttpETagHandler;
import io.github.chumper.webserver.handler.HttpFileHandler;
import io.github.chumper.webserver.handler.HttpKeepAliveHandler;
import io.github.chumper.webserver.handler.HttpRequestLogHandler;
import io.github.chumper.webserver.handler.HttpRootHandler;
import io.github.chumper.webserver.util.ConsoleLogger;
import io.github.chumper.webserver.util.Logger;

/**
* Main bootstrap class used to parse all configurations, to configure and start the server
*/
public class Main {

private static final Logger logger = new ConsoleLogger();

public static void main(String... args)
throws ExecutionException, InterruptedException, IOException {

// Load the configuration from the environment as well as the arguments
Config config = ConfigFactory.load();

// create an http server
HttpServer server = new HttpServer(
config.getInt("server.port"),
config.getInt("server.threads")
);

// add handler that will work on /
server.addHttpHandler(new HttpRootHandler());

// if comments are active, add handler for that and give it a mongo repository
if(config.getBoolean("server.comments.active")) {
server.addHttpHandler(new HttpCommentHandler(
new MongoDbCommentRepository(
config.getString("server.comments.host"),
config.getInt("server.comments.port")
)
));
}

// if files are active add the handler and the etag handling
if(config.getBoolean("server.files.active")) {
server.addHttpHandler(new HttpFileHandler(config.getString("server.files.root")));
server.addHttpHandler(new HttpETagHandler());
}

// add the keep alive handler so sockets can be reused
server.addHttpHandler(new HttpKeepAliveHandler());

// add logging handler if active
if(config.getBoolean("server.logging.active")) {
server.addHttpHandler(new HttpRequestLogHandler());
}

// start the server and wait until it is started
server.start().get();

logger.log("Press RETURN to stop the server");

BufferedReader br = new BufferedReader(new InputStreamReader(System.in));

br.readLine();

// Stop the server
server.stop();
}
}
43 changes: 43 additions & 0 deletions src/main/java/io/github/chumper/webserver/core/HttpServer.java
Original file line number Diff line number Diff line change
@@ -0,0 +1,43 @@
package io.github.chumper.webserver.core;

import io.github.chumper.webserver.core.http.HttpHandler;
import io.github.chumper.webserver.core.http.HttpRequest;
import io.github.chumper.webserver.core.pipeline.HttpPipeline;

/**
* The {@link HttpServer} extends the {@link Server} and adds handler that will interpret the
* incoming messages as HTTP requests.
*/
public class HttpServer
extends Server {

/**
* The {@link HttpServer} will add a {@link HttpPipeline} to the server and accepts custom {@link
* HttpHandler} for processing the {@link HttpRequest}
*/
private HttpPipeline httpPipeline = new HttpPipeline();

/**
* Creates a server that will listen on the given port when started
*
* @param port The port number to listen on
* @param threads The number of threads for the worker pool
*/
public HttpServer(int port,
int threads) {
super(port, threads);
}

/**
* Will add the given handler to the Http pipeline
*/
public void addHttpHandler(HttpHandler httpHandler) {
this.httpPipeline.addHttpHandler(httpHandler);
}

@Override
protected void registerHandler() {
// configure the server with the HTTP Pipeline
addPipelineHandler(httpPipeline);
}
}
169 changes: 169 additions & 0 deletions src/main/java/io/github/chumper/webserver/core/Server.java
Original file line number Diff line number Diff line change
@@ -0,0 +1,169 @@
package io.github.chumper.webserver.core;

import java.io.IOException;
import java.net.InetSocketAddress;
import java.net.ServerSocket;
import java.util.ArrayList;
import java.util.List;
import java.util.concurrent.CompletableFuture;
import java.util.concurrent.CountDownLatch;
import java.util.concurrent.ExecutorService;
import java.util.concurrent.Executors;

import io.github.chumper.webserver.core.pipeline.SocketHandler;
import io.github.chumper.webserver.util.ConsoleLogger;
import io.github.chumper.webserver.util.Logger;

/**
* This is the base class for all servers. Subclasses need to populate the pipeline with their
* custom handlers and can overwrite methods if needed
*/
public abstract class Server {

/**
* The port the server will listen on
*/
private int port;
/**
* Used to start the server async and provide a method to listen when the server started and is
* ready to accept requests
*/
private CountDownLatch startLatch = new CountDownLatch(1);
/**
* The executor that is responsible to process each request, this is the worker pool
*/
private ExecutorService executor;
/**
* The socked that accepts the incomming messages
*/
private ServerSocket ss;
/**
* Simple logger to log all events
*/
private Logger logger = new ConsoleLogger();
/**
* The list of handlers that will transform the request and do stuff like HTTP parsing and
* processing
*/
private List<SocketHandler> socketHandlers = new ArrayList<>();

/**
* Creates a server that will listen on the given port when started
*
* @param port The port number to listen on
* @param threads The number of threads for the worker pool
*/
public Server(int port,
int threads) {
this.port = port;
this.executor = Executors.newFixedThreadPool(threads);
}

/**
* Will add the given handler to the server processing pipeline
*
* @param handler The handler that transforms the request
*/
public void addPipelineHandler(SocketHandler handler) {
this.socketHandlers.add(handler);
}

/**
* Will start the server in a new thread and returns a future that will complete when the server
* has been started
*
* @return A Future that completes when the server has been started
*/
public CompletableFuture<Boolean> start() {
logger.log("Starting server on port {}", port);

registerHandler();

SocketListenerThread socketListener = new SocketListenerThread(port);
socketListener.start();

return CompletableFuture.supplyAsync(() -> {
try {
startLatch.await();
} catch (InterruptedException e) {
logger.log("Error while server start: {}", e.getMessage());
}
return socketListener.isBound();
});
}

/**
* Will stop the server and closes the sockets and shutdown the worker threads. All buffered
* request will be discarded and no new connections will be accepted.
*/
public void stop() throws IOException {

ss.close();
executor.shutdown();

logger.log("Server stopped");
}

private class SocketListenerThread
extends Thread {

private int port;

SocketListenerThread(int port) {
this.port = port;
setName("SocketListenerThread");
}

public void run() {
try {
ss = new ServerSocket();
ss.setReuseAddress(true);
ss.setSoTimeout(0);
ss.bind(new InetSocketAddress(port), 20000);

logger.log("Server started on port {}, waiting for connections", port);

// count down the latch so that we can indicate that the server started
startLatch.countDown();

acceptData();
} catch (IOException e) {
// we dont want to clutter the console with stacktraces in this simple exercise
logger.log("Could not open the socket on port {}: {}", port, e.getMessage());
startLatch.countDown();
}
}

private void acceptData() {
while (true) {
try {
if (executor.isTerminated()) { break; }

executor.execute(new SocketProcessor(ss.accept(), socketHandlers));

} catch (IOException e) {
// I/O error in reading/writing data, or server closed while
// accepting data
if (!executor.isTerminated() && !ss.isClosed()) {
logger.log("Error occurred: {}", e.getMessage());
}
}
}
}

/**
* Will check whether the serverSocket is bound or not
*
* @return boolean true if the socket is bound, false otherwise
*/
boolean isBound() {
return ss.isBound();
}
}

/**
* This methods needs to be overriden by other subclasses where they implement their own handler
* pipeline
*/
protected abstract void registerHandler();
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,69 @@
package io.github.chumper.webserver.core;

import java.io.IOException;
import java.net.Socket;
import java.net.SocketException;
import java.util.List;

import io.github.chumper.webserver.core.pipeline.SocketHandler;
import io.github.chumper.webserver.util.ConsoleLogger;
import io.github.chumper.webserver.util.Logger;

/**
* The main thread that processes a socket, will handle the handler and the lifecycle
*/
class SocketProcessor
implements Runnable {

private final Logger logger = new ConsoleLogger();

/**
* The socket to manage
*/
private Socket socket;
/**
* A list of handlers that can manipulate the socket and interact with the manipulated request,
* similar to the netty pipeline but much more simple and less robust.
*/
private List<SocketHandler> socketHandlers;

SocketProcessor(Socket socket,
List<SocketHandler> socketHandlers) {
this.socket = socket;
this.socketHandlers = socketHandlers;
}

@Override
@SuppressWarnings("unchecked")
public void run() {
try {
// first we set a timeout so inactive sockets will not stop the server after a while
socket.setSoTimeout(3000);
// we will loop through all handlers until one closes the socket
while(!socket.isClosed()) {
SocketHandler.State state;
for (SocketHandler socketHandler : socketHandlers) {
state = socketHandler.process(socket);
// when a handler returns no response we will asume that the pipeline should be interrupted
// This could be more robust (e.g. exceptions or a pipeline status object) but should be enough for now.
if (state == SocketHandler.State.DISCARD) {
// closing of the socket is not the responsible of the handler it will be done in the
// finally block
return;
}
}
}
} catch (SocketException e) {
logger.log("Could not configure the socket: {}", e.getMessage());
} finally {
try {
// just in case a handler did not clean, we will do it here
socket.shutdownInput();
socket.shutdownOutput();
socket.close();
} catch (IOException e) {
logger.log("Could not close the socket: {}", e.getMessage());
}
}
}
}
Loading

0 comments on commit 0e403f7

Please sign in to comment.