Creating Persistent Connections with WebSockets

You can use WebSockets to create a persistent connection from a client (such as a mobile device or a computer) to an App Engine instance. The open connection allows two-way data exchange between the client and the server at any time, resulting in lower latency and better use of resources.

WebSockets

The WebSockets protocol, defined in RFC 6455, provides a full-duplex communication channel between a client and a server. The channel is initiated from an HTTP(S) request with an "upgrade" header.

Typical use cases for WebSockets include:

  • Real time event updates, such as social media feeds, sports scores, news, or stock market prices
  • User notifications, such as software or content updates
  • Chatting applications
  • Collaborative editing tools
  • Multiplayer games

WebSockets are always available to your application without any additional setup. Once a WebSockets connection is established, it will time out after one hour.

Running a sample application with WebSockets

First, follow the instructions in "Hello, World!" for Java on App Engine to set up your environment and project, and to understand how App Engine Java apps are structured.

Clone the sample app

Copy the sample apps to your local machine, and navigate to the websockets directory:

git clone https://github.com/GoogleCloudPlatform/java-docs-samples
cd java-docs-samples/appengine-java8/websocket-jetty/

Run the sample locally

To run the sample application on your local computer:

  1. Start the local Eclipse Jetty web server using the Jetty Maven plugin:

    mvn jetty:run-exploded
    
  2. In your web browser, enter the following address:

    http://localhost:8080
    

Deploy and run the sample on App Engine

To deploy your application to the App Engine flexible environment, run the following command from the websocket-jetty directory:

mvn appengine:deploy

The sample application is packaged as a jar, and runs automatically using the Java 8/Jetty 9 with Servlet 3.1 Runtime.

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

Sample code overeview

This sample describes creating a local-memory-only chat app using the native Jetty WebSockets server API and the native Jetty WebSockets client API.

app.yaml

In your app.yaml, set manual_scaling to 1 to ensure that only a single instance is used, so that this application works consistently with multiple users. To work across multiple instances, an extra-instance messaging system or data store would be needed.

Set the environment variable JETTY_MODULES_ENABLE:websocket to enable the Jetty WebSocket module on the Jetty server.

# Copyright 2018 Google LLC
#
# 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

ServerSocket.java

The sample application creates a server socket using the endpoint /echo. The homepage (/) provides a form to submit a text message to the server socket. A client-side socket sends the message to the server. On receiving the message, the server echoes the message back to the client.

/*
 * Copyright 2018 Google LLC
 *
 * 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.jettynative;

import java.io.IOException;
import java.util.logging.Logger;
import org.eclipse.jetty.websocket.api.Session;
import org.eclipse.jetty.websocket.api.WebSocketAdapter;

/*
 * Server-side WebSocket : echoes received message back to client.
 */
public class ServerSocket extends WebSocketAdapter {
  private Logger logger = Logger.getLogger(SendServlet.class.getName());

  @Override
  public void onWebSocketConnect(Session session) {
    super.onWebSocketConnect(session);
    logger.fine("Socket Connected: " + session);
  }

  @Override
  public void onWebSocketText(String message) {
    super.onWebSocketText(message);
    logger.fine("Received message: " + message);
    try {
      // echo message back to client
      getRemote().sendString(message);
    } catch (IOException e) {
      logger.severe("Error echoing message: " + e.getMessage());
    }
  }

  @Override
  public void onWebSocketClose(int statusCode, String reason) {
    super.onWebSocketClose(statusCode, reason);
    logger.fine("Socket Closed: [" + statusCode + "] " + reason);
  }

  @Override
  public void onWebSocketError(Throwable cause) {
    super.onWebSocketError(cause);
    logger.severe("Websocket error : " + cause.getMessage());
  }
}

SendServlet.java

This servlet converts the message sent from client over POST to use the WebSockets protocol.

/*
 * Copyright 2018 Google LLC
 *
 * 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.jettynative;

import com.google.common.base.Preconditions;
import java.io.IOException;
import java.net.URI;
import java.net.URISyntaxException;
import java.util.concurrent.Future;
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;
import org.eclipse.jetty.util.ssl.SslContextFactory;
import org.eclipse.jetty.websocket.api.Session;
import org.eclipse.jetty.websocket.api.WebSocketPolicy;
import org.eclipse.jetty.websocket.client.ClientUpgradeRequest;
import org.eclipse.jetty.websocket.client.WebSocketClient;
import org.eclipse.jetty.websocket.common.scopes.SimpleContainerScope;

@WebServlet("/send")
/** Servlet that sends the message sent over POST to over a websocket connection. */
public class SendServlet extends HttpServlet {

  private Logger logger = Logger.getLogger(SendServlet.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";
  // GAE_SERVICE environment variable is set to the GCP service name.
  private static final String GAE_SERVICE_ENV_VAR = "GAE_SERVICE";

  private final WebSocketClient webSocketClient;
  private final ClientSocket clientSocket;

  public SendServlet() {
    this.webSocketClient = createWebSocketClient();
    this.clientSocket = new ClientSocket();
  }

  @Override
  public void doPost(HttpServletRequest request, HttpServletResponse response) throws IOException {
    String message = request.getParameter("message");
    try {
      sendMessageOverWebSocket(message);
      response.sendRedirect("/");
    } catch (Exception e) {
      logger.severe("Error sending message over socket: " + e.getMessage());
      e.printStackTrace(response.getWriter());
      response.setStatus(HttpStatus.INTERNAL_SERVER_ERROR_500);
    }
  }

  private WebSocketClient createWebSocketClient() {
    WebSocketClient webSocketClient;
    if (System.getenv(GAE_INSTANCE_VAR) != null) {
      // If on HTTPS, create client with SSL Context
      SslContextFactory sslContextFactory = new SimpleContainerScope(
              WebSocketPolicy.newClientPolicy())
              .getSslContextFactory();
      webSocketClient = new WebSocketClient(sslContextFactory);
    } else {
      // local testing on HTTP
      webSocketClient = new WebSocketClient();
    }
    return webSocketClient;
  }

  private void sendMessageOverWebSocket(String message) throws Exception {
    if (!webSocketClient.isRunning()) {
      try {
        webSocketClient.start();
      } catch (URISyntaxException e) {
        e.printStackTrace();
      }
    }
    ClientUpgradeRequest request = new ClientUpgradeRequest();
    // Attempt connection
    Future<Session> future = webSocketClient.connect(clientSocket,
        new URI(getWebSocketAddress()), request);
    // Wait for Connect
    Session session = future.get();
    // Send a message
    session.getRemote().sendString(message);
    // Close session
    session.close();
  }

  /**
   * 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) {
        String serviceName = System.getenv(GAE_SERVICE_ENV_VAR);
        webSocketHost = serviceName + "-dot-" + 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;
  }
}

EchoServlet.java

This servlet defines a server-side WebSocket with the endpoint /echo.

/*
 * Copyright 2018 Google LLC
 *
 * 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.jettynative;

import javax.servlet.annotation.WebServlet;
import org.eclipse.jetty.websocket.servlet.WebSocketServlet;
import org.eclipse.jetty.websocket.servlet.WebSocketServletFactory;

/*
 * Server-side WebSocket registered as /echo servlet.
 */
@SuppressWarnings("serial")
@WebServlet(name = "Echo WebSocket Servlet", urlPatterns = { "/echo" })
public class EchoServlet extends WebSocketServlet {
  @Override
  public void configure(WebSocketServletFactory factory) {
    factory.register(ServerSocket.class);
  }
}

ClientSocket.java

The message received by the client is stored in an in-memory cache and is viewable on the homepage.

/*
 * Copyright 2018 Google LLC
 *
 * 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.jettynative;

import java.util.Collection;
import java.util.Collections;
import java.util.concurrent.ConcurrentLinkedDeque;
import java.util.logging.Logger;
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;

/**
 * Basic Echo Client Socket.
 */
@WebSocket(maxTextMessageSize = 64 * 1024)
public class ClientSocket {
  private Logger logger = Logger.getLogger(ClientSocket.class.getName());
  private Session session;
  // stores the messages in-memory.
  // Note : this is currently an in-memory store for demonstration,
  // not recommended for production use-cases.
  private static Collection<String> messages = new ConcurrentLinkedDeque<>();

  @OnWebSocketClose
  public void onClose(int statusCode, String reason) {
    logger.fine("Connection closed: " + statusCode + ":" + reason);
    this.session = null;
  }

  @OnWebSocketConnect
  public void onConnect(Session session) {
    this.session = session;
  }

  @OnWebSocketMessage
  public void onMessage(String msg) {
    logger.fine("Message Received : " + msg);
    messages.add(msg);
  }

  // Retrieve all received messages.
  public static Collection<String> getReceivedMessages() {
    return Collections.unmodifiableCollection(messages);
  }
}

js_client.jsp

The sample provides a JavaScript client that you can use to test the WebSockets server.

<!DOCTYPE html>
<!--
  Copyright 2018 Google LLC

  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.
-->
<html>
<%@ page import="com.example.flexible.websocket.jettynative.SendServlet" %>
  <head>
    <title>Google App Engine Flexible Environment - WebSocket Echo</title>
    <meta name="viewport" content="width=device-width, initial-scale=1.0">
  </head>
  <body>
    <p>Echo demo</p>
    <form id="echo-form">
      <textarea id="echo-text" placeholder="Enter some text..."></textarea>
      <button type="submit">Send</button>
    </form>

    <div>
      <p>Messages:</p>
      <ul id="echo-response"></ul>
    </div>

    <div>
      <p>Status:</p>
      <ul id="echo-status"></ul>
    </div>
    <script src="https://ajax.googleapis.com/ajax/libs/jquery/2.1.4/jquery.min.js"></script>
    <script>
    $(function() {
      var webSocketUri =  "<%=SendServlet.getWebSocketAddress() %>";

      /* Get elements from the page */
      var form = $('#echo-form');
      var textarea = $('#echo-text');
      var output = $('#echo-response');
      var status = $('#echo-status');

      /* Helper to keep an activity log on the page. */
      function log(text){
        status.append($('<li>').text(text))
      }

      /* Establish the WebSocket connection and register event handlers. */
      var websocket = new WebSocket(webSocketUri);

      websocket.onopen = function() {
        log('Connected : ' + webSocketUri);
      };

      websocket.onclose = function() {
        log('Closed');
      };

      websocket.onmessage = function(e) {
        log('Message received');
        output.append($('<li>').text(e.data));
      };

      websocket.onerror = function(e) {
        log('Error (see console)');
        console.log(e);
      };

      /* Handle form submission and send a message to the websocket. */
      form.submit(function(e) {
        e.preventDefault();
        var data = textarea.val();
        websocket.send(data);
      });
    });
    </script>
  </body>
</html>

index.jsp

This file creates a form to test the WebSockets application.

<!DOCTYPE html>
<!--
  Copyright 2018 Google LLC

  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.
-->
<%@ page import="com.example.flexible.websocket.jettynative.ClientSocket" %>
<html>
  <head>
    <meta http-equiv="refresh" content="10">
  </head>
  <title>Send a message </title>
  <body>
    <h3> Publish a message </h3>
    <form action="send" method="POST">
      <label for="message">Message:</label>
      <input id="message" type="input" name="message" />
      <input id="send"  type="submit" value="Send" />
    </form>
    <h3> Last received messages </h3>
    <%= ClientSocket.getReceivedMessages() %>
  </body>
</html>

pom.xml

This file contains information about the project's dependencies and the build target.

<!--
  Copyright 2018 Google LLC

  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.
-->
<project xmlns="http://maven.apache.org/POM/4.0.0"
  xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
  xsi:schemaLocation="http://maven.apache.org/POM/4.0.0 http://maven.apache.org/xsd/maven-4.0.0.xsd">
  <modelVersion>4.0.0</modelVersion>

  <groupId>org.eclipse.jetty.demo</groupId>
  <artifactId>native-jetty-websocket-example</artifactId>
  <version>1.0-SNAPSHOT</version>
  <packaging>war</packaging>

  <!--
   The parent pom defines common style checks and testing strategies for our samples.
   Removing or replacing it should not effect the execution of the samples in anyway.
 -->
 <parent>
   <groupId>com.google.cloud.samples</groupId>
   <artifactId>shared-configuration</artifactId>
   <version>1.0.10</version>
 </parent>

  <properties>
    <maven.compiler.target>1.8</maven.compiler.target>
    <maven.compiler.source>1.8</maven.compiler.source>
    <failOnMissingWebXml>false</failOnMissingWebXml> <!-- REQUIRED -->
    <appengine.maven.plugin>1.3.1</appengine.maven.plugin>
    <jetty.version>9.4.4.v20170414</jetty.version>
  </properties>

  <dependencies>
    <dependency>
      <groupId>javax.servlet</groupId>
      <artifactId>javax.servlet-api</artifactId>
      <version>3.1.0</version>
      <type>jar</type>
      <scope>provided</scope>
    </dependency>
    <!-- To run websockets client -->
    <dependency>
      <groupId>org.eclipse.jetty.websocket</groupId>
      <artifactId>websocket-client</artifactId>
      <version>${jetty.version}</version>
    </dependency>
    <dependency>
      <groupId>org.eclipse.jetty.websocket</groupId>
      <artifactId>websocket-servlet</artifactId>
      <version>${jetty.version}</version>
      <scope>provided</scope>
    </dependency>
    <dependency>
      <groupId>com.google.guava</groupId>
      <artifactId>guava</artifactId>
      <version>23.0</version>
    </dependency>
  </dependencies>

  <build>
    <!-- for hot reload of the web application -->
    <outputDirectory>${project.build.directory}/${project.build.finalName}/WEB-INF/classes
    </outputDirectory>
    <plugins>
      <!-- for deployment of web application -->
      <plugin>
        <groupId>com.google.cloud.tools</groupId>
        <artifactId>appengine-maven-plugin</artifactId>
        <version>${appengine.maven.plugin}</version>
        <configuration>
        </configuration>
      </plugin>
      <!-- for local testing of web application -->
      <plugin>
        <groupId>org.eclipse.jetty</groupId>
        <artifactId>jetty-maven-plugin</artifactId>
        <version>${jetty.version}</version>
      </plugin>
    </plugins>
  </build>
</project>

Session affinity

Not all clients support WebSockets. To work around this, many applications use libraries such as socket.io that fall back on http long polling with clients that don't support WebSockets.

App Engine typically distributes requests evenly among available instances. However, when using http long polling, multiple sequential requests from a given user need to reach the same instance.

To allow App Engine to send requests by the same user to the same instance, you can enable session affinity. App Engine then identifies which requests are sent by the same users by inspecting a cookie and routes those requests to the same instance.

Session affinity in App Engine is implemented on a best-effort basis. When developing your app, you should always assume that session affinity is not guaranteed. A client can lose affinity with the target instance in the following scenarios:

  • The App Engine autoscaler can add or remove instances that serve your application. The application might reallocate the load, and the target instance might move. To minimize this risk, ensure that you have set the minimum number of instances to handle the expected load.
  • If the target instance fails health checks, App Engine moves the session to a healthy instance.
  • Session affinity is lost when an instance is rebooted for maintenance or software updates. App Engine flexible environment VM instances are restarted on a weekly basis.

Because session affinity isn't guaranteed, you should only use it to take advantage of the ability of socket.io and other libraries to fall back on HTTP long polling in cases where the connection is broken. You should never use session affinity to build stateful applications.

Enabling and disabling session affinity

By default, session affinity is disabled for all App Engine applications. Session affinity is set at the version level of your application and can be enabled or disabled on deployment.

To enable session affinity for your App Engine version, add the following entry to your app.yaml file:

network:
  session_affinity: true

Once the version is deployed with the updated app.yaml, new requests will start serving from the same instance as long as that instance is available.

To turn off session affinity, remove the entry from your app.yaml file, or set the value to false:

network:
  session_affinity: false
Was this page helpful? Let us know how we did:

Send feedback about...

App Engine flexible environment for Java docs