-
Notifications
You must be signed in to change notification settings - Fork 0
Commit
This commit does not belong to any branch on this repository, and may belong to a fork outside of the repository.
- Loading branch information
n.plaschke
committed
Oct 16, 2016
0 parents
commit 0e403f7
Showing
37 changed files
with
2,554 additions
and
0 deletions.
There are no files selected for viewing
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
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 | ||
} |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
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
43
src/main/java/io/github/chumper/webserver/core/HttpServer.java
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
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
169
src/main/java/io/github/chumper/webserver/core/Server.java
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
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(); | ||
} |
69 changes: 69 additions & 0 deletions
69
src/main/java/io/github/chumper/webserver/core/SocketProcessor.java
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
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()); | ||
} | ||
} | ||
} | ||
} |
Oops, something went wrong.