diff --git a/websockets/README.md b/websockets/README.md
new file mode 100644
index 00000000000..11d77909e2a
--- /dev/null
+++ b/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/websockets/pom.xml b/websockets/pom.xml
new file mode 100644
index 00000000000..60ae944c576
--- /dev/null
+++ b/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/websockets/src/main/appengine/app.yaml b/websockets/src/main/appengine/app.yaml
new file mode 100644
index 00000000000..344b66572c1
--- /dev/null
+++ b/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/websockets/src/main/java/com/example/flexible/websocket/jsr356/ClientSocket.java b/websockets/src/main/java/com/example/flexible/websocket/jsr356/ClientSocket.java
new file mode 100644
index 00000000000..fc659699120
--- /dev/null
+++ b/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/websockets/src/main/java/com/example/flexible/websocket/jsr356/SendServlet.java b/websockets/src/main/java/com/example/flexible/websocket/jsr356/SendServlet.java
new file mode 100644
index 00000000000..22feba6a600
--- /dev/null
+++ b/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/websockets/src/main/java/com/example/flexible/websocket/jsr356/ServerSocket.java b/websockets/src/main/java/com/example/flexible/websocket/jsr356/ServerSocket.java
new file mode 100644
index 00000000000..2edd4ca08b5
--- /dev/null
+++ b/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/websockets/src/main/webapp/index.jsp b/websockets/src/main/webapp/index.jsp
new file mode 100644
index 00000000000..c673b4a2aab
--- /dev/null
+++ b/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/websockets/src/main/webapp/js_client.jsp b/websockets/src/main/webapp/js_client.jsp
new file mode 100644
index 00000000000..20e1016e20f
--- /dev/null
+++ b/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
+
+
+
+
+
+
+
+
+