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 Python on App Engine to set up your environment and project, and to understand how App Engine Python 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/python-docs-samples
cd python-docs-samples/appengine/flexible/websockets/

Run the sample locally

To run locally, you need to use Gunicorn with the flask_socket worker:

$ gunicorn -b 127.0.0.1:8080 -k flask_sockets.worker main:app

Deploy and run the sample on App Engine

To deploy your application to the App Engine flexible environment, run the following command from the directory where your app.yaml is located:

gcloud beta app deploy

You can then direct your browser to https://[YOUR_PROJECT_ID].appspot.com/

Sample code overeview

This sample describes creating a local-memory-only chat app using the Flask-Sockets WebSockets framework.

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.

To take advantage of WebSockets, use a special gunicorn worker class.

runtime: python
env: flex

# Use a special gunicorn worker class to support websockets.
entrypoint: gunicorn -b :$PORT -k flask_sockets.worker main:app

runtime_config:
  python_version: 3

# Use only a single instance, so that this local-memory-only chat app will work
# consistently with multiple users. To work across multiple instances, an
# extra-instance messaging system or data store would be needed.
manual_scaling:
  instances: 1


# 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

requirements.txt

requirements.txt and the Python package manager pip are used to declare and install application dependencies.

This application requires Flask, Flask-Sockets, and Gunicorn.

Flask==1.0.2
Flask-Sockets==0.2.1
gunicorn==19.9.0
requests==2.21.0

main.py

This sample is a Flask application using flask_sockets to establish a WebSockets connection, receive a message from the client, and send the message back.

# 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.

from __future__ import print_function

from flask import Flask, render_template
from flask_sockets import Sockets


app = Flask(__name__)
sockets = Sockets(app)


@sockets.route('/chat')
def chat_socket(ws):
    while not ws.closed:
        message = ws.receive()
        if message is None:  # message is "None" if the client has closed.
            continue
        # Send the message to all clients connected to this webserver
        # process. (To support multiple processes or instances, an
        # extra-instance storage or messaging system would be required.)
        clients = ws.handler.server.clients.values()
        for client in clients:
            client.ws.send(message)


@app.route('/')
def index():
    return render_template('index.html')


if __name__ == '__main__':
    print("""
This can not be run directly because the Flask development server does not
support web sockets. Instead, use gunicorn:

gunicorn -b 127.0.0.1:8080 -k flask_sockets.worker main:app

""")

main_test.py

The following file creates and tests a WebSockets 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.

import socket
import subprocess

import pytest
import requests
from retrying import retry
import websocket


@pytest.fixture(scope='module')
def server():
    """Provides the address of a test HTTP/websocket server.
    The test server is automatically created before
    a test and destroyed at the end.
    """
    # Ask the OS to allocate a port.
    sock = socket.socket()
    sock.bind(('127.0.0.1', 0))
    port = sock.getsockname()[1]

    # Free the port and pass it to a subprocess.
    sock.close()

    bind_to = '127.0.0.1:{}'.format(port)
    server = subprocess.Popen(
        ['gunicorn', '-b', bind_to, '-k' 'flask_sockets.worker', 'main:app'])

    # Wait until the server responds before proceeding.
    @retry(wait_fixed=50, stop_max_delay=5000)
    def check_server(url):
        requests.get(url)

    check_server('http://{}/'.format(bind_to))

    yield bind_to

    server.kill()


def test_http(server):
    result = requests.get('http://{}/'.format(server))
    assert 'Python Websockets Chat' in result.text


def test_websocket(server):
    url = 'ws://{}/chat'.format(server)
    ws_one = websocket.WebSocket()
    ws_one.connect(url)

    ws_two = websocket.WebSocket()
    ws_two.connect(url)

    message = 'Hello, World'
    ws_one.send(message)

    assert ws_one.recv() == message
    assert ws_two.recv() == message

index.html

You can use this html form to test the application:

{#
# 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.
#}
<!doctype html>
<html>
  <head>
    <title>Google App Engine Flexible Environment - Python Websockets Chat</title>
    <meta name="viewport" content="width=device-width, initial-scale=1.0">
  </head>
  <body>

    <p>Chat demo</p>
    <form id="chat-form">
      <textarea id="chat-text" placeholder="Enter some text..."></textarea>
      <button type="submit">Send</button>
    </form>

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

    <div>
      <p>Status:</p>
      <ul id="chat-status"></ul>
    </div>

    <script src="https://ajax.googleapis.com/ajax/libs/jquery/2.1.4/jquery.min.js"></script>
    <script>
    $(function() {
      /* If the main page is served via https, the WebSocket must be served via
         "wss" (WebSocket Secure) */
      var scheme = window.location.protocol == "https:" ? 'wss://' : 'ws://';
      var webSocketUri =  scheme
                          + window.location.hostname
                          + (location.port ? ':'+location.port: '')
                          + '/chat';

      /* Get elements from the page */
      var form = $('#chat-form');
      var textarea = $('#chat-text');
      var output = $('#chat-response');
      var status = $('#chat-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');
      };

      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>

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 Python docs