From 5d3640c4de2f286fe98b8c9c06c3e82f8294cdad Mon Sep 17 00:00:00 2001 From: labadav Date: Fri, 9 Feb 2018 14:45:47 -0800 Subject: [PATCH] WebSockets samples for Java --- appengine/websockets/README.md | 57 +++++++++ appengine/websockets/pom.xml | 83 +++++++++++++ .../websockets/src/main/appengine/app.yaml | 34 ++++++ .../websocket/jsr356/ClientSocket.java | 110 ++++++++++++++++++ .../websocket/jsr356/SendServlet.java | 67 +++++++++++ .../websocket/jsr356/ServerSocket.java | 90 ++++++++++++++ .../websockets/src/main/webapp/index.jsp | 33 ++++++ .../websockets/src/main/webapp/js_client.jsp | 85 ++++++++++++++ 8 files changed, 559 insertions(+) create mode 100644 appengine/websockets/README.md create mode 100644 appengine/websockets/pom.xml create mode 100644 appengine/websockets/src/main/appengine/app.yaml create mode 100644 appengine/websockets/src/main/java/com/example/flexible/websocket/jsr356/ClientSocket.java create mode 100644 appengine/websockets/src/main/java/com/example/flexible/websocket/jsr356/SendServlet.java create mode 100644 appengine/websockets/src/main/java/com/example/flexible/websocket/jsr356/ServerSocket.java create mode 100644 appengine/websockets/src/main/webapp/index.jsp create mode 100644 appengine/websockets/src/main/webapp/js_client.jsp diff --git a/appengine/websockets/README.md b/appengine/websockets/README.md new file mode 100644 index 00000000000..11d77909e2a --- /dev/null +++ b/appengine/websockets/README.md @@ -0,0 +1,57 @@ +# App Engine Flexible Environment - Web Socket Example +This sample demonstrates how to use [Websockets](https://tools.ietf.org/html/rfc6455) on [Google App Engine Flexible Environment](https://cloud.google.com/appengine/docs/flexible/java/) using Java. +The sample uses the [JSR-356](https://www.jcp.org/en/jsr/detail?id=356) Java API for the Websocket [client](https://mvnrepository.com/artifact/org.eclipse.jetty.websocket/javax-websocket-client-impl). + +## Sample application workflow + +1. The sample application creates a server socket using the endpoint `/echo`. +1. The homepage (`/`) provides a form to submit a text message to the server socket. This creates a client-side socket +and sends the message to the server. +1. The server on receiving the message, echoes the message back to the client. +1. The message received by the client is stored in an in-memory cache and is viewable on the homepage. + +The sample also provides a Javascript [client](src/main/webapp/js_client.jsp)(`/js_client.jsp`) that you can use to test against the Websocket server. + +## Setup + + - [Install](https://cloud.google.com/sdk/) and initialize GCloud SDK. This will + ``` + gcloud init + ``` +- If this is your first time creating an app engine application + ``` + gcloud appengine create + ``` + +## Local testing + +Run using the [Jetty Maven plugin](http://www.eclipse.org/jetty/documentation/9.4.x/jetty-maven-plugin.html). +``` +mvn jetty:run +``` +You can then direct your browser to `http://localhost:8080/` + +To test the Javascript client, access `http://localhost:8080/js_client.jsp` + +## App Engine Flex Deployment + +#### `app.yaml` Configuration + +App Engine Flex deployment configuration is provided in [app.yaml](src/main/appengine/app.yaml). + +Set the environment variable `JETTY_MODULES_ENABLE:websocket` to enable the Jetty websocket module on the Jetty server. + +Manual scaling is set to a single instance as we are using an in-memory cache of messages for this sample application. + +For more details on configuring your `app.yaml`, please refer to [this resource](https://cloud.google.com/appengine/docs/flexible/nodejs/configuring-your-app-with-app-yaml). + +#### Deploy + +The sample application is packaged as a war, and hence will be automatically run using the [Java 8/Jetty 9 with Servlet 3.1 Runtime](https://cloud.google.com/appengine/docs/flexible/java/dev-jetty9). + +``` + mvn appengine:deploy +``` +You can then direct your browser to `https://YOUR_PROJECT_ID.appspot.com/` + +To test the Javascript client, access `https://YOUR_PROJECT_ID.appspot.com/js_client.jsp` diff --git a/appengine/websockets/pom.xml b/appengine/websockets/pom.xml new file mode 100644 index 00000000000..60ae944c576 --- /dev/null +++ b/appengine/websockets/pom.xml @@ -0,0 +1,83 @@ + + + 4.0.0 + 1.0-SNAPSHOT + com.example.flexible + websocket + war + + + com.google.cloud + appengine-flexible + 1.0.0 + .. + + + + 1.8 + 1.8 + false + 1.3.1 + 9.4.4.v20170414 + + + + + javax.servlet + javax.servlet-api + 3.1.0 + jar + provided + + + + + org.eclipse.jetty.websocket + javax-websocket-client-impl + ${jetty.version} + + + + + com.google.guava + guava + 20.0 + + + + + + ${project.build.directory}/${project.build.finalName}/WEB-INF/classes + + + + + com.google.cloud.tools + appengine-maven-plugin + ${appengine.maven.plugin} + + + + + + org.eclipse.jetty + jetty-maven-plugin + ${jetty.version} + + + + diff --git a/appengine/websockets/src/main/appengine/app.yaml b/appengine/websockets/src/main/appengine/app.yaml new file mode 100644 index 00000000000..344b66572c1 --- /dev/null +++ b/appengine/websockets/src/main/appengine/app.yaml @@ -0,0 +1,34 @@ +# Copyright 2017 Google Inc. +# +# 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. + +runtime: java +env: flex +manual_scaling: + instances: 1 + +handlers: +- url: /.* + script: this field is required, but ignored + +env_variables: + JETTY_MODULES_ENABLE: websocket + + +# For applications which can take advantage of session affinity +# (where the load balancer will attempt to route multiple connections from +# the same user to the same App Engine instance), uncomment the folowing: + +# network: +# session_affinity: true + diff --git a/appengine/websockets/src/main/java/com/example/flexible/websocket/jsr356/ClientSocket.java b/appengine/websockets/src/main/java/com/example/flexible/websocket/jsr356/ClientSocket.java new file mode 100644 index 00000000000..fc659699120 --- /dev/null +++ b/appengine/websockets/src/main/java/com/example/flexible/websocket/jsr356/ClientSocket.java @@ -0,0 +1,110 @@ +/* + * Copyright 2017 Google Inc. + * + * 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 com.example.flexible.websocket.jsr356; + +import com.google.common.util.concurrent.SettableFuture; + +import java.net.URI; +import java.util.Collection; +import java.util.Collections; +import java.util.concurrent.ConcurrentLinkedDeque; +import java.util.concurrent.ExecutionException; +import java.util.logging.Logger; +import javax.websocket.ClientEndpoint; +import javax.websocket.CloseReason; +import javax.websocket.ContainerProvider; +import javax.websocket.OnClose; +import javax.websocket.OnError; +import javax.websocket.OnMessage; +import javax.websocket.OnOpen; +import javax.websocket.Session; +import javax.websocket.WebSocketContainer; + +/** + * Web socket client example using JSR-356 Java WebSocket API. Sends a message to the server, and + * stores the echoed messages received from the server. + */ +@ClientEndpoint +public class ClientSocket { + + private static final Logger logger = Logger.getLogger(ClientSocket.class.getName()); + + // stores the messages in-memory. + // Note : this is currently an in-memory store for demonstration, + // not recommended for production use-cases. + private static Collection messages = new ConcurrentLinkedDeque<>(); + + private SettableFuture future = SettableFuture.create(); + private Session session; + + ClientSocket(URI endpointUri) { + try { + WebSocketContainer container = ContainerProvider.getWebSocketContainer(); + session = container.connectToServer(this, endpointUri); + } catch (Exception e) { + throw new RuntimeException(e); + } + } + + @OnOpen + public void onOpen(Session session) { + future.set(true); + } + + /** + * Handles message received from the server. + * @param message server message in String format + * @param session current session + */ + @OnMessage + public void onMessage(String message, Session session) { + logger.fine("Received message from server : " + message); + messages.add(message); + } + + boolean waitOnOpen() throws InterruptedException, ExecutionException { + // wait on handling onOpen + boolean opened = future.get(); + logger.fine("Connected to server"); + return opened; + } + + @OnClose + public void onClose(CloseReason reason, Session session) { + logger.fine("Closing Web Socket: " + reason.getReasonPhrase()); + } + + void sendMessage(String str) { + try { + // Send a message to the server + logger.fine("Sending message : " + str); + session.getAsyncRemote().sendText(str); + } catch (Exception e) { + logger.severe("Error sending message : " + e.getMessage()); + } + } + + // Retrieve all received messages. + public static Collection getReceivedMessages() { + return Collections.unmodifiableCollection(messages); + } + + @OnError + public void logErrors(Throwable t) { + logger.severe(t.getMessage()); + } +} diff --git a/appengine/websockets/src/main/java/com/example/flexible/websocket/jsr356/SendServlet.java b/appengine/websockets/src/main/java/com/example/flexible/websocket/jsr356/SendServlet.java new file mode 100644 index 00000000000..22feba6a600 --- /dev/null +++ b/appengine/websockets/src/main/java/com/example/flexible/websocket/jsr356/SendServlet.java @@ -0,0 +1,67 @@ +/* + * Copyright 2017 Google Inc. + * + * 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 com.example.flexible.websocket.jsr356; + +import com.google.common.base.Preconditions; +import java.io.IOException; +import java.net.URI; +import java.net.URISyntaxException; +import java.util.logging.Logger; +import javax.servlet.annotation.WebServlet; +import javax.servlet.http.HttpServlet; +import javax.servlet.http.HttpServletRequest; +import javax.servlet.http.HttpServletResponse; +import org.eclipse.jetty.http.HttpStatus; + +@WebServlet("/send") +/** Servlet that converts the message sent over POST to be over websocket. */ +public class SendServlet extends HttpServlet { + + private Logger logger = Logger.getLogger(SendServlet.class.getName()); + private final String webSocketAddress = ServerSocket.getWebSocketAddress(); + private ClientSocket clientSocket; + + private void initializeWebSocket() throws Exception { + clientSocket = new ClientSocket(new URI(webSocketAddress)); + clientSocket.waitOnOpen(); + logger.info("REST service: open websocket client at " + webSocketAddress); + } + + private void sendMessageOverWebSocket(String message) throws Exception { + if (clientSocket == null) { + try { + initializeWebSocket(); + } catch (URISyntaxException e) { + e.printStackTrace(); + } + } + clientSocket.sendMessage(message); + } + + @Override + public void doPost(HttpServletRequest request, HttpServletResponse response) throws IOException { + String message = request.getParameter("message"); + Preconditions.checkNotNull(message); + try { + sendMessageOverWebSocket(message); + response.sendRedirect("/"); + } catch (Exception e) { + e.printStackTrace(response.getWriter()); + response.setStatus(HttpStatus.INTERNAL_SERVER_ERROR_500); + } + } +} diff --git a/appengine/websockets/src/main/java/com/example/flexible/websocket/jsr356/ServerSocket.java b/appengine/websockets/src/main/java/com/example/flexible/websocket/jsr356/ServerSocket.java new file mode 100644 index 00000000000..2edd4ca08b5 --- /dev/null +++ b/appengine/websockets/src/main/java/com/example/flexible/websocket/jsr356/ServerSocket.java @@ -0,0 +1,90 @@ +/* + * Copyright 2017 Google Inc. + * + * 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 com.example.flexible.websocket.jsr356; + +import com.google.common.base.Preconditions; +import java.io.IOException; +import java.util.logging.Logger; +import javax.websocket.CloseReason; +import javax.websocket.OnClose; +import javax.websocket.OnMessage; +import javax.websocket.OnOpen; +import javax.websocket.Session; +import javax.websocket.server.ServerEndpoint; + +/** + * WebSocket server example using JSR-356 Java WebSocket API. Echoes back the message received over + * the websocket back to the client. + */ +@ServerEndpoint("/echo") +public class ServerSocket { + + private static final Logger logger = Logger.getLogger(ServerSocket.class.getName()); + private static final String ENDPOINT = "/echo"; + private static final String WEBSOCKET_PROTOCOL_PREFIX = "ws://"; + private static final String WEBSOCKET_HTTPS_PROTOCOL_PREFIX = "wss://"; + private static final String APPENGINE_HOST_SUFFIX = ".appspot.com"; + // GAE_INSTANCE environment is used to detect App Engine Flexible Environment + private static final String GAE_INSTANCE_VAR = "GAE_INSTANCE"; + // GOOGLE_CLOUD_PROJECT environment variable is set to the GCP project ID on App Engine Flexible. + private static final String GOOGLE_CLOUD_PROJECT_ENV_VAR = "GOOGLE_CLOUD_PROJECT"; + + @OnOpen + public void onOpen(Session session) { + logger.info("WebSocket Opened: " + session.getId()); + } + + /** + * Handle a message received from the client, and echo back to the client. + * @param message Message in text format + * @param session Current active session + * @throws IOException error sending message back to client + */ + @OnMessage + public void onMessage(String message, Session session) throws IOException { + logger.fine("Message Received : " + message); + // echo message back to the client + session.getAsyncRemote().sendText(message); + } + + @OnClose + public void onClose(CloseReason reason, Session session) { + logger.fine("Closing WebSocket: " + reason.getReasonPhrase()); + } + + /** + * Returns the host:port/echo address a client needs to use to communicate with the server. + * On App engine Flex environments, result will be in the form wss://project-id.appspot.com/echo + */ + public static String getWebSocketAddress() { + // Use ws://127.0.0.1:8080/echo when testing locally + String webSocketHost = "127.0.0.1:8080"; + String webSocketProtocolPrefix = WEBSOCKET_PROTOCOL_PREFIX; + + // On App Engine flexible environment, use wss://project-id.appspot.com/echo + if (System.getenv(GAE_INSTANCE_VAR) != null) { + String projectId = System.getenv(GOOGLE_CLOUD_PROJECT_ENV_VAR); + if (projectId != null) { + webSocketHost = projectId + APPENGINE_HOST_SUFFIX; + } + Preconditions.checkNotNull(webSocketHost); + // Use wss:// instead of ws:// protocol when connecting over https + webSocketProtocolPrefix = WEBSOCKET_HTTPS_PROTOCOL_PREFIX; + } + return webSocketProtocolPrefix + webSocketHost + ENDPOINT; + } +} diff --git a/appengine/websockets/src/main/webapp/index.jsp b/appengine/websockets/src/main/webapp/index.jsp new file mode 100644 index 00000000000..c673b4a2aab --- /dev/null +++ b/appengine/websockets/src/main/webapp/index.jsp @@ -0,0 +1,33 @@ + + +<%@ page import="com.example.flexible.websocket.jsr356.ClientSocket" %> + + + + + Send a message + +

Publish a message

+
+ + + +
+

Last received messages

+ <%= ClientSocket.getReceivedMessages() %> + + diff --git a/appengine/websockets/src/main/webapp/js_client.jsp b/appengine/websockets/src/main/webapp/js_client.jsp new file mode 100644 index 00000000000..20e1016e20f --- /dev/null +++ b/appengine/websockets/src/main/webapp/js_client.jsp @@ -0,0 +1,85 @@ + + + +<%@ page import="com.example.flexible.websocket.jsr356.ServerSocket" %> + + Google App Engine Flexible Environment - WebSocket Echo + + + +

Echo demo

+
+ + +
+ +
+

Messages:

+
    +
    + +
    +

    Status:

    +
      +
      + + + +