Skip to content

Commit 0e403f7

Browse files
author
n.plaschke
committed
added httpserver
0 parents  commit 0e403f7

37 files changed

+2554
-0
lines changed

build.gradle

Lines changed: 29 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,29 @@
1+
group 'io.github.chumper'
2+
version '1.0-SNAPSHOT'
3+
4+
apply plugin: 'java'
5+
6+
sourceCompatibility = 1.8
7+
8+
repositories {
9+
mavenCentral()
10+
}
11+
12+
dependencies {
13+
testCompile group: 'junit', name: 'junit', version: '4.11'
14+
testCompile group: 'org.apache.httpcomponents', name: 'httpclient', version: '4.5.2'
15+
testCompile group: 'org.apache.httpcomponents', name: 'fluent-hc', version: '4.5.2'
16+
17+
compile "com.typesafe:config:1.3.1"
18+
compile "org.mongodb:mongo-java-driver:3.3.0"
19+
}
20+
21+
//create a single Jar with all dependencies
22+
task fatJar(type: Jar) {
23+
manifest {
24+
attributes 'Main-Class': 'io.github.chumper.webserver.Main'
25+
}
26+
baseName = project.name + '-all'
27+
from { configurations.compile.collect { it.isDirectory() ? it : zipTree(it) } }
28+
with jar
29+
}
Lines changed: 80 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,80 @@
1+
package io.github.chumper.webserver;
2+
3+
import com.typesafe.config.Config;
4+
import com.typesafe.config.ConfigFactory;
5+
6+
import java.io.BufferedReader;
7+
import java.io.IOException;
8+
import java.io.InputStreamReader;
9+
import java.util.concurrent.ExecutionException;
10+
11+
import io.github.chumper.webserver.core.HttpServer;
12+
import io.github.chumper.webserver.data.MongoDbCommentRepository;
13+
import io.github.chumper.webserver.handler.HttpCommentHandler;
14+
import io.github.chumper.webserver.handler.HttpETagHandler;
15+
import io.github.chumper.webserver.handler.HttpFileHandler;
16+
import io.github.chumper.webserver.handler.HttpKeepAliveHandler;
17+
import io.github.chumper.webserver.handler.HttpRequestLogHandler;
18+
import io.github.chumper.webserver.handler.HttpRootHandler;
19+
import io.github.chumper.webserver.util.ConsoleLogger;
20+
import io.github.chumper.webserver.util.Logger;
21+
22+
/**
23+
* Main bootstrap class used to parse all configurations, to configure and start the server
24+
*/
25+
public class Main {
26+
27+
private static final Logger logger = new ConsoleLogger();
28+
29+
public static void main(String... args)
30+
throws ExecutionException, InterruptedException, IOException {
31+
32+
// Load the configuration from the environment as well as the arguments
33+
Config config = ConfigFactory.load();
34+
35+
// create an http server
36+
HttpServer server = new HttpServer(
37+
config.getInt("server.port"),
38+
config.getInt("server.threads")
39+
);
40+
41+
// add handler that will work on /
42+
server.addHttpHandler(new HttpRootHandler());
43+
44+
// if comments are active, add handler for that and give it a mongo repository
45+
if(config.getBoolean("server.comments.active")) {
46+
server.addHttpHandler(new HttpCommentHandler(
47+
new MongoDbCommentRepository(
48+
config.getString("server.comments.host"),
49+
config.getInt("server.comments.port")
50+
)
51+
));
52+
}
53+
54+
// if files are active add the handler and the etag handling
55+
if(config.getBoolean("server.files.active")) {
56+
server.addHttpHandler(new HttpFileHandler(config.getString("server.files.root")));
57+
server.addHttpHandler(new HttpETagHandler());
58+
}
59+
60+
// add the keep alive handler so sockets can be reused
61+
server.addHttpHandler(new HttpKeepAliveHandler());
62+
63+
// add logging handler if active
64+
if(config.getBoolean("server.logging.active")) {
65+
server.addHttpHandler(new HttpRequestLogHandler());
66+
}
67+
68+
// start the server and wait until it is started
69+
server.start().get();
70+
71+
logger.log("Press RETURN to stop the server");
72+
73+
BufferedReader br = new BufferedReader(new InputStreamReader(System.in));
74+
75+
br.readLine();
76+
77+
// Stop the server
78+
server.stop();
79+
}
80+
}
Lines changed: 43 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,43 @@
1+
package io.github.chumper.webserver.core;
2+
3+
import io.github.chumper.webserver.core.http.HttpHandler;
4+
import io.github.chumper.webserver.core.http.HttpRequest;
5+
import io.github.chumper.webserver.core.pipeline.HttpPipeline;
6+
7+
/**
8+
* The {@link HttpServer} extends the {@link Server} and adds handler that will interpret the
9+
* incoming messages as HTTP requests.
10+
*/
11+
public class HttpServer
12+
extends Server {
13+
14+
/**
15+
* The {@link HttpServer} will add a {@link HttpPipeline} to the server and accepts custom {@link
16+
* HttpHandler} for processing the {@link HttpRequest}
17+
*/
18+
private HttpPipeline httpPipeline = new HttpPipeline();
19+
20+
/**
21+
* Creates a server that will listen on the given port when started
22+
*
23+
* @param port The port number to listen on
24+
* @param threads The number of threads for the worker pool
25+
*/
26+
public HttpServer(int port,
27+
int threads) {
28+
super(port, threads);
29+
}
30+
31+
/**
32+
* Will add the given handler to the Http pipeline
33+
*/
34+
public void addHttpHandler(HttpHandler httpHandler) {
35+
this.httpPipeline.addHttpHandler(httpHandler);
36+
}
37+
38+
@Override
39+
protected void registerHandler() {
40+
// configure the server with the HTTP Pipeline
41+
addPipelineHandler(httpPipeline);
42+
}
43+
}
Lines changed: 169 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,169 @@
1+
package io.github.chumper.webserver.core;
2+
3+
import java.io.IOException;
4+
import java.net.InetSocketAddress;
5+
import java.net.ServerSocket;
6+
import java.util.ArrayList;
7+
import java.util.List;
8+
import java.util.concurrent.CompletableFuture;
9+
import java.util.concurrent.CountDownLatch;
10+
import java.util.concurrent.ExecutorService;
11+
import java.util.concurrent.Executors;
12+
13+
import io.github.chumper.webserver.core.pipeline.SocketHandler;
14+
import io.github.chumper.webserver.util.ConsoleLogger;
15+
import io.github.chumper.webserver.util.Logger;
16+
17+
/**
18+
* This is the base class for all servers. Subclasses need to populate the pipeline with their
19+
* custom handlers and can overwrite methods if needed
20+
*/
21+
public abstract class Server {
22+
23+
/**
24+
* The port the server will listen on
25+
*/
26+
private int port;
27+
/**
28+
* Used to start the server async and provide a method to listen when the server started and is
29+
* ready to accept requests
30+
*/
31+
private CountDownLatch startLatch = new CountDownLatch(1);
32+
/**
33+
* The executor that is responsible to process each request, this is the worker pool
34+
*/
35+
private ExecutorService executor;
36+
/**
37+
* The socked that accepts the incomming messages
38+
*/
39+
private ServerSocket ss;
40+
/**
41+
* Simple logger to log all events
42+
*/
43+
private Logger logger = new ConsoleLogger();
44+
/**
45+
* The list of handlers that will transform the request and do stuff like HTTP parsing and
46+
* processing
47+
*/
48+
private List<SocketHandler> socketHandlers = new ArrayList<>();
49+
50+
/**
51+
* Creates a server that will listen on the given port when started
52+
*
53+
* @param port The port number to listen on
54+
* @param threads The number of threads for the worker pool
55+
*/
56+
public Server(int port,
57+
int threads) {
58+
this.port = port;
59+
this.executor = Executors.newFixedThreadPool(threads);
60+
}
61+
62+
/**
63+
* Will add the given handler to the server processing pipeline
64+
*
65+
* @param handler The handler that transforms the request
66+
*/
67+
public void addPipelineHandler(SocketHandler handler) {
68+
this.socketHandlers.add(handler);
69+
}
70+
71+
/**
72+
* Will start the server in a new thread and returns a future that will complete when the server
73+
* has been started
74+
*
75+
* @return A Future that completes when the server has been started
76+
*/
77+
public CompletableFuture<Boolean> start() {
78+
logger.log("Starting server on port {}", port);
79+
80+
registerHandler();
81+
82+
SocketListenerThread socketListener = new SocketListenerThread(port);
83+
socketListener.start();
84+
85+
return CompletableFuture.supplyAsync(() -> {
86+
try {
87+
startLatch.await();
88+
} catch (InterruptedException e) {
89+
logger.log("Error while server start: {}", e.getMessage());
90+
}
91+
return socketListener.isBound();
92+
});
93+
}
94+
95+
/**
96+
* Will stop the server and closes the sockets and shutdown the worker threads. All buffered
97+
* request will be discarded and no new connections will be accepted.
98+
*/
99+
public void stop() throws IOException {
100+
101+
ss.close();
102+
executor.shutdown();
103+
104+
logger.log("Server stopped");
105+
}
106+
107+
private class SocketListenerThread
108+
extends Thread {
109+
110+
private int port;
111+
112+
SocketListenerThread(int port) {
113+
this.port = port;
114+
setName("SocketListenerThread");
115+
}
116+
117+
public void run() {
118+
try {
119+
ss = new ServerSocket();
120+
ss.setReuseAddress(true);
121+
ss.setSoTimeout(0);
122+
ss.bind(new InetSocketAddress(port), 20000);
123+
124+
logger.log("Server started on port {}, waiting for connections", port);
125+
126+
// count down the latch so that we can indicate that the server started
127+
startLatch.countDown();
128+
129+
acceptData();
130+
} catch (IOException e) {
131+
// we dont want to clutter the console with stacktraces in this simple exercise
132+
logger.log("Could not open the socket on port {}: {}", port, e.getMessage());
133+
startLatch.countDown();
134+
}
135+
}
136+
137+
private void acceptData() {
138+
while (true) {
139+
try {
140+
if (executor.isTerminated()) { break; }
141+
142+
executor.execute(new SocketProcessor(ss.accept(), socketHandlers));
143+
144+
} catch (IOException e) {
145+
// I/O error in reading/writing data, or server closed while
146+
// accepting data
147+
if (!executor.isTerminated() && !ss.isClosed()) {
148+
logger.log("Error occurred: {}", e.getMessage());
149+
}
150+
}
151+
}
152+
}
153+
154+
/**
155+
* Will check whether the serverSocket is bound or not
156+
*
157+
* @return boolean true if the socket is bound, false otherwise
158+
*/
159+
boolean isBound() {
160+
return ss.isBound();
161+
}
162+
}
163+
164+
/**
165+
* This methods needs to be overriden by other subclasses where they implement their own handler
166+
* pipeline
167+
*/
168+
protected abstract void registerHandler();
169+
}
Lines changed: 69 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,69 @@
1+
package io.github.chumper.webserver.core;
2+
3+
import java.io.IOException;
4+
import java.net.Socket;
5+
import java.net.SocketException;
6+
import java.util.List;
7+
8+
import io.github.chumper.webserver.core.pipeline.SocketHandler;
9+
import io.github.chumper.webserver.util.ConsoleLogger;
10+
import io.github.chumper.webserver.util.Logger;
11+
12+
/**
13+
* The main thread that processes a socket, will handle the handler and the lifecycle
14+
*/
15+
class SocketProcessor
16+
implements Runnable {
17+
18+
private final Logger logger = new ConsoleLogger();
19+
20+
/**
21+
* The socket to manage
22+
*/
23+
private Socket socket;
24+
/**
25+
* A list of handlers that can manipulate the socket and interact with the manipulated request,
26+
* similar to the netty pipeline but much more simple and less robust.
27+
*/
28+
private List<SocketHandler> socketHandlers;
29+
30+
SocketProcessor(Socket socket,
31+
List<SocketHandler> socketHandlers) {
32+
this.socket = socket;
33+
this.socketHandlers = socketHandlers;
34+
}
35+
36+
@Override
37+
@SuppressWarnings("unchecked")
38+
public void run() {
39+
try {
40+
// first we set a timeout so inactive sockets will not stop the server after a while
41+
socket.setSoTimeout(3000);
42+
// we will loop through all handlers until one closes the socket
43+
while(!socket.isClosed()) {
44+
SocketHandler.State state;
45+
for (SocketHandler socketHandler : socketHandlers) {
46+
state = socketHandler.process(socket);
47+
// when a handler returns no response we will asume that the pipeline should be interrupted
48+
// This could be more robust (e.g. exceptions or a pipeline status object) but should be enough for now.
49+
if (state == SocketHandler.State.DISCARD) {
50+
// closing of the socket is not the responsible of the handler it will be done in the
51+
// finally block
52+
return;
53+
}
54+
}
55+
}
56+
} catch (SocketException e) {
57+
logger.log("Could not configure the socket: {}", e.getMessage());
58+
} finally {
59+
try {
60+
// just in case a handler did not clean, we will do it here
61+
socket.shutdownInput();
62+
socket.shutdownOutput();
63+
socket.close();
64+
} catch (IOException e) {
65+
logger.log("Could not close the socket: {}", e.getMessage());
66+
}
67+
}
68+
}
69+
}

0 commit comments

Comments
 (0)