Permalink
Browse files

Added basic WebSocket support in the embedded server

  • Loading branch information...
1 parent da1d18a commit d8fa2d0543e40532d3becc1eb696d68532b223cb @nacx nacx committed Jul 17, 2015
View
@@ -10,3 +10,4 @@ bin
*.ipr
*.iws
reports
+.java-version
View
@@ -60,6 +60,17 @@
<version>${jetty.version}</version>
</dependency>
+ <dependency>
+ <groupId>org.eclipse.jetty.websocket</groupId>
+ <artifactId>websocket-server</artifactId>
+ <version>${jetty.version}</version>
+ </dependency>
+ <dependency>
+ <groupId>org.eclipse.jetty.websocket</groupId>
+ <artifactId>websocket-servlet</artifactId>
+ <version>${jetty.version}</version>
+ </dependency>
+
<!-- JUNIT DEPENDENCY FOR TESTING -->
<dependency>
<groupId>junit</groupId>
@@ -85,6 +96,12 @@
<version>2.2.4</version>
<scope>test</scope>
</dependency>
+ <dependency>
+ <groupId>org.eclipse.jetty.websocket</groupId>
+ <artifactId>websocket-client</artifactId>
+ <version>${jetty.version}</version>
+ <scope>test</scope>
+ </dependency>
</dependencies>
<build>
@@ -1,5 +1,10 @@
package spark;
+import static java.util.Objects.requireNonNull;
+
+import java.util.HashMap;
+import java.util.Map;
+import java.util.Optional;
import java.util.concurrent.CountDownLatch;
import org.slf4j.Logger;
@@ -32,9 +37,12 @@
protected static String staticFileFolder = null;
protected static String externalStaticFileFolder = null;
+ protected static Map<String, Class<?>> webSocketHandlers = null;
+
protected static int maxThreads = -1;
protected static int minThreads = -1;
protected static int threadIdleTimeoutMillis = -1;
+ protected static Optional<Integer> webSocketIdleTimeoutMillis = Optional.empty();
protected static SparkServer server;
protected static SimpleRouteMatcher routeMatcher;
@@ -241,6 +249,38 @@ public static synchronized void externalStaticFileLocation(String externalFolder
}
/**
+ * Maps the given path to the given WebSocket handler.
+ * <p>
+ * This is currently only available in the embedded server mode.
+ *
+ * @param path the WebSocket path.
+ * @param handler the handler class that will manage the WebSocket connection to the given path.
+ */
+ public static synchronized void webSocket(String path, Class<?> handler) {
+ requireNonNull(path, "WebSocket path cannot be null");
+ requireNonNull(handler, "WebSocket handler class cannot be null");
+ if (initialized) {
+ throwBeforeRouteMappingException();
+ }
+ if (runFromServlet) {
+ throw new IllegalStateException("WebSockets are only supported in the embedded server");
+ }
+ if (webSocketHandlers == null) {
+ webSocketHandlers = new HashMap<>();
+ }
+ webSocketHandlers.put(path, handler);
+ }
+
+ /**
+ * Sets the max idle timeout in milliseconds for WebSocket connections.
+ *
+ * @param timeoutMillis The max idle timeout in milliseconds.
+ */
+ public static void webSocketIdleTimeoutMillis(int timeoutMillis) {
+ webSocketIdleTimeoutMillis = Optional.of(timeoutMillis);
+ }
+
+ /**
* Waits for the spark server to be initialized.
* If it's already initialized will return immediately
*/
@@ -358,7 +398,7 @@ protected static void addFilter(String httpMethod, FilterImpl filter) {
+ "'", filter.getAcceptType(), filter);
}
- private static synchronized void init() {
+ public static synchronized void init() {
if (!initialized) {
routeMatcher = RouteMatcherFactory.get();
new Thread(new Runnable() {
@@ -377,7 +417,9 @@ public void run() {
latch,
maxThreads,
minThreads,
- threadIdleTimeoutMillis);
+ threadIdleTimeoutMillis,
+ webSocketHandlers,
+ webSocketIdleTimeoutMillis);
}
}).start();
initialized = true;
@@ -20,6 +20,8 @@
import java.net.ServerSocket;
import java.util.ArrayList;
import java.util.List;
+import java.util.Map;
+import java.util.Optional;
import java.util.concurrent.CountDownLatch;
import java.util.concurrent.TimeUnit;
@@ -29,9 +31,13 @@
import org.eclipse.jetty.server.ServerConnector;
import org.eclipse.jetty.server.handler.HandlerList;
import org.eclipse.jetty.server.handler.ResourceHandler;
+import org.eclipse.jetty.servlet.ServletContextHandler;
import org.eclipse.jetty.util.resource.Resource;
import org.eclipse.jetty.util.ssl.SslContextFactory;
import org.eclipse.jetty.util.thread.QueuedThreadPool;
+import org.eclipse.jetty.websocket.server.WebSocketUpgradeFilter;
+import org.eclipse.jetty.websocket.server.pathmap.ServletPathSpec;
+import org.eclipse.jetty.websocket.servlet.WebSocketCreator;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
@@ -71,6 +77,7 @@ public SparkServer(Handler handler) {
* @param maxThreads - max nbr of threads.
* @param minThreads - min nbr of threads.
* @param threadIdleTimeoutMillis - idle timeout (ms).
+ * @param webSockerIdleTimeoutMIllis - Optional WebSocket idle timeout (ms).
*/
public void ignite(String host,
int port,
@@ -83,7 +90,9 @@ public void ignite(String host,
CountDownLatch latch,
int maxThreads,
int minThreads,
- int threadIdleTimeoutMillis) {
+ int threadIdleTimeoutMillis,
+ Map<String, Class<?>> webSocketHandlers,
+ Optional<Integer> webSockerIdleTimeoutMillis) {
if (port == 0) {
try (ServerSocket s = new ServerSocket(0)) {
@@ -114,8 +123,26 @@ public void ignite(String host,
server = connector.getServer();
server.setConnectors(new Connector[] {connector});
+ ServletContextHandler webSocketServletContextHandler = null;
+ if (webSocketHandlers != null) {
+ try {
+ webSocketServletContextHandler = new ServletContextHandler(null, "/", true, false);
+ WebSocketUpgradeFilter wsfilter = WebSocketUpgradeFilter.configureContext(webSocketServletContextHandler);
+ if (webSockerIdleTimeoutMillis.isPresent()) {
+ wsfilter.getFactory().getPolicy().setIdleTimeout(webSockerIdleTimeoutMillis.get());
+ }
+ for (String path : webSocketHandlers.keySet()) {
+ WebSocketCreator wscreator = WebSocketCreatorFactory.create(webSocketHandlers.get(path));
+ wsfilter.addMapping(new ServletPathSpec(path), wscreator);
+ }
+ } catch (Exception ex) {
+ logger.error("ignite failed", ex);
+ System.exit(100); // NOSONAR
+ }
+ }
+
// Handle static file routes
- if (staticFilesFolder == null && externalFilesFolder == null) {
+ if (staticFilesFolder == null && externalFilesFolder == null && webSocketServletContextHandler == null) {
server.setHandler(handler);
} else {
List<Handler> handlersInList = new ArrayList<Handler>();
@@ -127,6 +154,11 @@ public void ignite(String host,
// Set external static file location
setExternalStaticFileLocationIfPresent(externalFilesFolder, handlersInList);
+ // WebSocket handler must be the last one
+ if (webSocketServletContextHandler != null) {
+ handlersInList.add(webSocketServletContextHandler);
+ }
+
HandlerList handlers = new HandlerList();
handlers.setHandlers(handlersInList.toArray(new Handler[handlersInList.size()]));
server.setHandler(handlers);
@@ -0,0 +1,61 @@
+/*
+ * Copyright 2011- Per Wendel
+ *
+ * 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.
+ */
+package spark.webserver;
+
+import java.util.Objects;
+
+import org.eclipse.jetty.websocket.servlet.ServletUpgradeRequest;
+import org.eclipse.jetty.websocket.servlet.ServletUpgradeResponse;
+import org.eclipse.jetty.websocket.servlet.WebSocketCreator;
+
+/**
+ * Factory class to create {@link WebSocketCreator} implementations that
+ * delegate to the given handler class.
+ *
+ * @author Ignasi Barrera
+ */
+public class WebSocketCreatorFactory {
+
+ /**
+ * Creates a {@link WebSocketCreator} that uses the given handler class for
+ * the WebSocket connections.
+ *
+ * @param handlerClass The handler to use to manage WebSocket connections.
+ * @return The WebSocketCreator.
+ */
+ public static WebSocketCreator create(Class<?> handlerClass) {
+ try {
+ Object handler = handlerClass.newInstance();
+ return new SparkWebSocketCreator(handler);
+ } catch (InstantiationException | IllegalAccessException ex) {
+ throw new RuntimeException( "Could not instantiate websocket handler", ex);
+ }
+ }
+
+ private static class SparkWebSocketCreator implements WebSocketCreator {
+ private final Object handler;
+
+ private SparkWebSocketCreator(Object handler) {
+ this.handler = Objects.requireNonNull(handler, "handler cannot be null");
+ }
+
+ @Override
+ public Object createWebSocket(ServletUpgradeRequest request,
+ ServletUpgradeResponse response) {
+ return handler;
+ }
+ }
+}
@@ -4,10 +4,15 @@
import java.io.File;
import java.io.FileWriter;
import java.io.IOException;
+import java.net.URI;
import java.nio.ByteBuffer;
import java.util.HashMap;
+import java.util.List;
import java.util.Map;
+import java.util.concurrent.TimeUnit;
+import org.eclipse.jetty.websocket.client.ClientUpgradeRequest;
+import org.eclipse.jetty.websocket.client.WebSocketClient;
import org.junit.AfterClass;
import org.junit.Assert;
import org.junit.BeforeClass;
@@ -20,7 +25,8 @@
import spark.examples.exception.SubclassOfBaseException;
import spark.util.SparkTestUtil;
import spark.util.SparkTestUtil.UrlResponse;
-
+import spark.websocket.WebSocketTestClient;
+import spark.websocket.WebSocketTestHandler;
import static spark.Spark.after;
import static spark.Spark.before;
import static spark.Spark.exception;
@@ -30,6 +36,7 @@
import static spark.Spark.post;
import static spark.SparkBase.externalStaticFileLocation;
import static spark.SparkBase.staticFileLocation;
+import static spark.SparkBase.webSocket;
public class GenericIntegrationTest {
@@ -61,6 +68,7 @@ public static void setup() throws IOException {
staticFileLocation("/public");
externalStaticFileLocation(System.getProperty("java.io.tmpdir"));
+ webSocket("/ws", WebSocketTestHandler.class);
before("/secretcontent/*", (request, response) -> {
halt(401, "Go Away!");
@@ -450,4 +458,25 @@ public void testNotFoundExceptionMapper() throws Exception {
Assert.assertEquals(NOT_FOUND_BRO, response.body);
Assert.assertEquals(404, response.status);
}
+
+ @Test
+ public void testWebSocketConversation() throws Exception {
+ String uri = "ws://localhost:4567/ws";
+ WebSocketClient client = new WebSocketClient();
+ WebSocketTestClient ws = new WebSocketTestClient();
+
+ try {
+ client.start();
+ client.connect(ws, URI.create(uri), new ClientUpgradeRequest());
+ ws.awaitClose(30, TimeUnit.SECONDS);
+ } finally {
+ client.stop();
+ }
+
+ List<String> events = WebSocketTestHandler.events;
+ Assert.assertEquals(3, events.size(), 3);
+ Assert.assertEquals("onConnect", events.get(0));
+ Assert.assertEquals("onMessage: Hi Spark!", events.get(1));
+ Assert.assertEquals("onClose: 1000 Bye!", events.get(2));
+ }
}
@@ -0,0 +1,46 @@
+/*
+ * Copyright 2011- Per Wendel
+ *
+ * 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.
+ */
+package spark.examples.websocket;
+
+import java.io.IOException;
+
+import org.eclipse.jetty.websocket.api.Session;
+import org.eclipse.jetty.websocket.api.annotations.OnWebSocketClose;
+import org.eclipse.jetty.websocket.api.annotations.OnWebSocketConnect;
+import org.eclipse.jetty.websocket.api.annotations.OnWebSocketMessage;
+import org.eclipse.jetty.websocket.api.annotations.WebSocket;
+
+@WebSocket
+public class EchoWebSocket {
+ private Session session;
+
+ @OnWebSocketConnect
+ public void onConnect(Session session) {
+ this.session = session;
+ }
+
+ @OnWebSocketClose
+ public void onClose(int statusCode, String reason) {
+ this.session = null;
+ }
+
+ @OnWebSocketMessage
+ public void onMessage(String message) throws IOException {
+ System.out.println("Got: " + message);
+ session.getRemote().sendString(message);
+ }
+}
Oops, something went wrong.

0 comments on commit d8fa2d0

Please sign in to comment.