Implement real-time messaging with WebSocket

Updated at:
Copy as MD

WebSocket is a network protocol that provides a full-duplex communication channel over a single TCP connection. It creates a persistent connection between a client and a server, allowing both to send and receive data at any time. This avoids the overhead and latency of repeatedly establishing new connections, making it more efficient than the traditional HTTP request-response model. WebSocket is primarily used in applications that require real-time communication. ALB supports the WebSocket protocol by default.

Introduction to WebSocket

Why use WebSocket

As web technologies evolve, applications increasingly require servers to push data in real time for features like live chat rooms and real-time comments. The traditional polling method, where a client's browser repeatedly sends HTTP requests to a server to fetch the latest data, has significant drawbacks. Frequent requests with large HTTP headers and small data payloads increase server load and waste bandwidth.

To address these issues, HTML5 introduced the WebSocket protocol, which offers a more efficient solution for client-server communication. WebSocket supports full-duplex communication, meaning the server and client can send and receive data simultaneously. This allows the server to proactively push new data to the client without waiting for a polling request. This bi-directional, real-time communication mechanism improves data transfer efficiency, reduces unnecessary network requests, and saves server resources and bandwidth, providing a smoother and more responsive user experience.

Key features of WebSocket

Communication begins with a standard three-way TCP handshake. The client then sends a special HTTP request, known as a protocol upgrade handshake. Once this handshake is successful, the connection switches from HTTP to WebSocket. All subsequent communication between the client and server uses the WebSocket protocol, allowing for bi-directional data exchange over the same connection.

Once a WebSocket connection is established, it remains active. This persistent, low-latency connection allows for continuous, bi-directional data transfer, which improves data exchange efficiency.

image

WebSocket communicates using data frames, which have their own frame protocol format with concise headers. Data can be transmitted as text or binary. This method reduces protocol overhead on persistent connections, making network interaction more efficient. It saves server resources and bandwidth while providing a smoother real-time interactive experience.

For more information about the WebSocket protocol, see the official documentation The WebSocket Protocol.

WebSocket use cases

WebSocket is ideal for applications that require fast, real-time, bi-directional communication, such as AI applications, online chat rooms, real-time notification systems, multiplayer online games, and real-time market data feeds.

Example scenario

A company wants to deploy a web-based online chat application on Alibaba Cloud. Users can access the backend service through a domain name for real-time communication. As an instant messaging application, it requires low latency and efficient, real-time, bi-directional communication.

The company's website service faces challenges with high concurrency and persistent connection management. As the number of users grows, the traditional HTTP model cannot support many concurrent users in real-time communication because each interaction requires a new connection. This leads to a sharp increase in server load and poor performance.

In this scenario, using ALB with the WebSocket protocol effectively manages persistent connections under high concurrency. Deploying the application across a server group and using Redis for message synchronization ensures high availability. This provides a reliable and efficient real-time messaging solution for the online chat room application.

image

Usage notes

HTTP listeners on an ALB instance support the WebSocket protocol by default. ALB supports rolling updates, which means that configuration changes do not interrupt existing persistent connections.

Note the following:

  • If the connection between an ALB instance and a backend server uses a specific HTTP version, such as HTTP/1.1, we recommend that the backend server also support that HTTP version.

  • The default connection request timeout for an HTTP listener is 60 seconds. If no data is exchanged between ALB and a backend server for more than 60 seconds, ALB closes the connection.

    • If the default 60-second timeout is insufficient for your needs, you can modify the listener's Connection Request Timeout.

    • To keep the connection open, you must implement a keepalive mechanism to exchange packets at least once every 60 seconds.

Prerequisites

  • An internet-facing ALB instance is required. For more information, see Create and manage an ALB instance.

  • Three ECS instances are required: ECS01, ECS02, and ECS03.

    • ECS01 and ECS02 are used to deploy the WebSocket application, and ECS03 is used to deploy Redis.

    • In this tutorial, all servers run CentOS 7.9.

    • We recommend that you place ECS01, ECS02, and ECS03 in the same security group. If they are in different security groups, make sure to configure rules that allow traffic on the required communication ports between the servers.

  • A registered domain name with a completed ICP filing is required. For more information, see Register an Alibaba Cloud domain name and ICP filing.

Procedure

Step 1: Deploy services

You need to deploy Redis on your ECS03 instance and the WebSocket application on your ECS01 and ECS02 instances.

This topic uses a simple Python-based online chat room on CentOS 7.9 for demonstration purposes. This example is for reference only. In a production environment, use your own application.

Deploy Redis on ECS03

  1. Log on to the ECS03 instance.

  2. Copy and run the following commands to install and configure Redis.

    # Install EPEL (Extra Packages for Enterprise Linux)
    sudo yum install epel-release -y
    # Install Redis
    sudo yum install redis -y
    # Start and enable the Redis service
    sudo systemctl start redis
    sudo systemctl enable redis
    # Edit the Redis configuration file to allow remote connections
    sudo sed -i 's/^bind 127.0.0.1$/bind 0.0.0.0/' /etc/redis.conf
    sudo sed -i 's/^protected-mode yes/protected-mode no/' /etc/redis.conf
    # Restart the Redis service for the changes to take effect
    sudo systemctl restart redis
    # Check the Redis status
    sudo systemctl status redis
    
  3. If the commands run without errors and the output shows the Redis service is in the active (running) state, the deployment and configuration are successful.

    ● redis.service - Redis persistent key-value database
       Loaded: loaded (/usr/lib/systemd/system/redis.service; enabled; vendor preset: disabled)
       Active: active (running) since ...
     Main PID: 12345 (redis-server)
       CGroup: /system.slice/redis.service
               └─12345 /usr/bin/redis-server 0.0.0.0:6379
    

Deploy WebSocket on ECS01

  1. Log on to the ECS01 instance.

  2. Run sudo pip3 install flask flask-socketio flask-cors redis to install the dependencies.

  3. Run vi ECS01_ws.py and press the i key to enter edit mode.

  4. Copy and paste the following code:

    Sample code for the test service

    Note

    On line 13, replace the IP address in redis_url with the IP address of your Redis server (ECS03).

    import os
    import redis
    from flask import Flask, render_template, request
    from flask_cors import CORS
    from flask_socketio import SocketIO, emit, disconnect
    app = Flask(__name__)
    app.config['SECRET_KEY'] = 'secret!'
    # Enable Cross-Origin Resource Sharing (CORS)
    CORS(app)
    # Configure Redis as the message queue and state store
    redis_url = "redis://192.168.*.*:6379/0"  # Replace with the IP address of your Redis server
    redis_client = redis.StrictRedis.from_url(redis_url)
    # Set the log level to DEBUG for easier debugging
    socketio = SocketIO(app, message_queue=redis_url, manage_session=True, logger=True, engineio_logger=True, cors_allowed_origins="*")
    SESSION_PREFIX = "session:"
    def set_session_data(session_id, key, value):
        redis_client.hset(f"{SESSION_PREFIX}{session_id}", key, value)
    def get_session_data(session_id, key):
        return redis_client.hget(f"{SESSION_PREFIX}{session_id}", key)
    def delete_session_data(session_id):
        redis_client.delete(f"{SESSION_PREFIX}{session_id}")
    @app.route('/')
    def index():
        return render_template('index.html')
    @socketio.on('connect')
    def handle_connect():
        try:
            session_id = request.sid  # Get the client's session ID
            print(f"Session {session_id} connected.")
            welcome_message = "You have entered the chat room!"
            emit('message', welcome_message)
            set_session_data(session_id, "username", '')  # Initialize the username as empty
        except Exception as e:
            print(f"Error during connection: {str(e)}")
    @socketio.on('disconnect')
    def handle_disconnect():
        try:
            session_id = request.sid
            username = get_session_data(session_id, "username")
            if username:
                username = username.decode()
                leave_message = f"{username} has left the chat room."
                emit('message', leave_message, broadcast=True)
                print(leave_message)
            delete_session_data(session_id)
            print(f"Session {session_id} disconnected.")
        except Exception as e:
            print(f"Error during disconnection: {str(e)}")
    @socketio.on('set_username')
    def handle_set_username(username):
        session_id = request.sid
        set_session_data(session_id, "username", username)
        print(f"Username for client {session_id} set to {username}")
        emit('message', f"Your username has been set to: {username}")
    @socketio.on('message')
    def handle_message(msg):
        session_id = request.sid
        username = get_session_data(session_id, "username")
        if username:
            username = username.decode()
            formatted_message = f"{username}: {msg}"
            emit('message', formatted_message, broadcast=True)
            print(formatted_message)
        else:
            warning_message = "Failed to send message: Please set a username first."
            emit('message', warning_message)
            print(warning_message)
    if __name__ == '__main__':
        # Make sure the templates directory exists
        if not os.path.exists('templates'):
            os.makedirs('templates')
        # Use the Flask template (index.html)
        html_code = '''<!DOCTYPE html>
    <html lang="en">
    <head>
        <meta charset="UTF-8">
        <meta name="viewport" content="width=device-width, initial-scale=1.0">
        <title>Chat Room</title>
        <style>
            body {
                font-family: Arial, sans-serif;
                display: flex;
                flex-direction: column;
                align-items: center;
                margin: 0;
                padding: 0;
                background-color: #f0f0f0;
            }
            h1 {
                color: #333;
            }
            .chat-container {
                width: 90%;
                max-width: 600px;
                background: white;
                padding: 20px;
                border-radius: 8px;
                box-shadow: 0 0 10px rgba(0, 0, 0, 0.1);
            }
            .user-container, .message-container {
                display: flex;
                margin-bottom: 10px;
            }
            .user-container input, .message-container input {
                flex: 1;
                padding: 10px;
                margin-right: 10px;
                border: 1px solid #ccc;
                border-radius: 4px;
            }
            .message-container {
                margin-top: 10px;
            }
            button {
                padding: 10px;
                background-color: #0056b3;
                color: white;
                border: none;
                border-radius: 4px;
                cursor: pointer;
            }
            button:hover {
                background-color: #004099;
            }
            #messages {
                border: 1px solid #ccc;
                padding: 10px;
                height: 300px;
                overflow-y: scroll;
                margin-bottom: 10px;
                border-radius: 4px;
                background-color: #f9f9f9;
            }
        </style>
        <script src="https://cdnjs.cloudflare.com/ajax/libs/socket.io/4.0.1/socket.io.js"></script>
        <script src="https://code.jquery.com/jquery-3.6.0.min.js"></script>
    </head>
    <body>
        <h1>Online Chat Room</h1>
        <div class="chat-container">
            <div class="user-container">
                <input type="text" id="username" autocomplete="off" placeholder="Enter a username">
                <button onclick="setUsername()">Set Username</button>
            </div>
            <div id="messages"></div>
            <div class="message-container">
                <input type="text" id="myMessage" autocomplete="off" placeholder="Enter a message...">
                <button onclick="sendMessage()">Send</button>
            </div>
        </div>
        <script>
            var socket = io({ transports: ['websocket', 'polling', 'flashsocket'] });
            var usernameSet = false;
            socket.on('connect', function() {
                console.log("Connected to the server!");
                socket.on('message', function(msg){
                    $('#messages').append($('<div>').text(msg));
                    $('#messages').scrollTop($('#messages')[0].scrollHeight);
                });
            });
            function setUsername() {
                var username = $('#username').val();
                if (username) {
                    socket.emit('set_username', username);
                    usernameSet = true;  // Mark that username is set.
                } else {
                    alert("Username cannot be empty!");
                }
            }
            function sendMessage() {
                if (usernameSet) {
                    var message = $('#myMessage').val();
                    if (message) {
                        socket.send(message);
                        $('#myMessage').val('');
                    } else {
                        alert("Message cannot be empty!");
                    }
                } else {
                    alert("Please set a username first!");
                }
            }
        </script>
    </body>
    </html>
    '''
        # Save the template to a file
        with open('templates/index.html', 'w') as file:
            file.write(html_code)
        socketio.run(app, host='0.0.0.0', port=5000)
    
  5. Press the Esc key, and then enter :wq to save the changes.

  6. Run the sudo python3 ECS01_ws.py command to run the script.

  7. When the following output is displayed, the WebSocket application has started on port 5000.

    Server initialized for threading.
     * Serving Flask app 'ECS01_ws' (lazy loading)
     * Environment: production
       WARNING: This is a development server. Do not use it in a production deployment.
       Use a production WSGI server instead.
     * Debug mode: off
     * Running on all addresses.
       WARNING: This is a development server. Do not use it in a production deployment.
     * Running on http://192.168.*.*:5000/ (Press CTRL+C to quit)
    

    If the application fails to start, check if the port is already in use or if you copied the commands or code incorrectly.

● redis.service – Redis persistent key-value database
   Loaded: loaded (/usr/lib/systemd/system/redis.service; enabled; vendor preset: disabled)
  Drop-In: /etc/systemd/redis.service.d
           └─limit.conf
   Active: active (running) since Thu 2xxx xxx xxx xxx CST; 6s ago
  Process: 14715 ExecStop=/usr/libexec/redis-shutdown (code=exited, status=0/SUCCESS)
 Main PID: 14730 (redis-server)
   CGroup: /system.slice/redis.service
           └─14730 /usr/bin/redis-server 0.0.0.0:6379

Deploy WebSocket on ECS02

  1. Log on to the ECS02 instance.

  2. Run sudo pip3 install flask flask-socketio flask-cors redis to install the dependencies.

  3. Run vi ECS02_ws.py and press the i key to enter edit mode.

  4. Copy and paste the following code:

    Sample code for the test service

    Note

    On line 13, replace the IP address in redis_url with the IP address of your Redis server (ECS03).

    import os
    import redis
    from flask import Flask, render_template, request
    from flask_cors import CORS
    from flask_socketio import SocketIO, emit, disconnect
    app = Flask(__name__)
    app.config['SECRET_KEY'] = 'secret!'
    # Enable Cross-Origin Resource Sharing (CORS)
    CORS(app)
    # Configure Redis as the message queue and state store
    redis_url = "redis://192.168.*.*:6379/0"  # Replace with the IP address of your Redis server
    redis_client = redis.StrictRedis.from_url(redis_url)
    # Set the log level to DEBUG for easier debugging
    socketio = SocketIO(app, message_queue=redis_url, manage_session=True, logger=True, engineio_logger=True, cors_allowed_origins="*")
    SESSION_PREFIX = "session:"
    def set_session_data(session_id, key, value):
        redis_client.hset(f"{SESSION_PREFIX}{session_id}", key, value)
    def get_session_data(session_id, key):
        return redis_client.hget(f"{SESSION_PREFIX}{session_id}", key)
    def delete_session_data(session_id):
        redis_client.delete(f"{SESSION_PREFIX}{session_id}")
    @app.route('/')
    def index():
        return render_template('index.html')
    @socketio.on('connect')
    def handle_connect():
        try:
            session_id = request.sid  # Get the client's session ID
            print(f"Session {session_id} connected.")
            welcome_message = "You have entered the chat room!"
            emit('message', welcome_message)
            set_session_data(session_id, "username", '')  # Initialize the username as empty
        except Exception as e:
            print(f"Error during connection: {str(e)}")
    @socketio.on('disconnect')
    def handle_disconnect():
        try:
            session_id = request.sid
            username = get_session_data(session_id, "username")
            if username:
                username = username.decode()
                leave_message = f"{username} has left the chat room."
                emit('message', leave_message, broadcast=True)
                print(leave_message)
            delete_session_data(session_id)
            print(f"Session {session_id} disconnected.")
        except Exception as e:
            print(f"Error during disconnection: {str(e)}")
    @socketio.on('set_username')
    def handle_set_username(username):
        session_id = request.sid
        set_session_data(session_id, "username", username)
        print(f"Username for client {session_id} set to {username}")
        emit('message', f"Your username has been set to: {username}")
    @socketio.on('message')
    def handle_message(msg):
        session_id = request.sid
        username = get_session_data(session_id, "username")
        if username:
            username = username.decode()
            formatted_message = f"{username}: {msg}"
            emit('message', formatted_message, broadcast=True)
            print(formatted_message)
        else:
            warning_message = "Failed to send message: Please set a username first."
            emit('message', warning_message)
            print(warning_message)
    if __name__ == '__main__':
        # Make sure the templates directory exists
        if not os.path.exists('templates'):
            os.makedirs('templates')
        # Use the Flask template (index.html)
        html_code = '''<!DOCTYPE html>
    <html lang="en">
    <head>
        <meta charset="UTF-8">
        <meta name="viewport" content="width=device-width, initial-scale=1.0">
        <title>Chat Room</title>
        <style>
            body {
                font-family: Arial, sans-serif;
                display: flex;
                flex-direction: column;
                align-items: center;
                margin: 0;
                padding: 0;
                background-color: #f0f0f0;
            }
            h1 {
                color: #333;
            }
            .chat-container {
                width: 90%;
                max-width: 600px;
                background: white;
                padding: 20px;
                border-radius: 8px;
                box-shadow: 0 0 10px rgba(0, 0, 0, 0.1);
            }
            .user-container, .message-container {
                display: flex;
                margin-bottom: 10px;
            }
            .user-container input, .message-container input {
                flex: 1;
                padding: 10px;
                margin-right: 10px;
                border: 1px solid #ccc;
                border-radius: 4px;
            }
            .message-container {
                margin-top: 10px;
            }
            button {
                padding: 10px;
                background-color: #0056b3;
                color: white;
                border: none;
                border-radius: 4px;
                cursor: pointer;
            }
            button:hover {
                background-color: #004099;
            }
            #messages {
                border: 1px solid #ccc;
                padding: 10px;
                height: 300px;
                overflow-y: scroll;
                margin-bottom: 10px;
                border-radius: 4px;
                background-color: #f9f9f9;
            }
        </style>
        <script src="https://cdnjs.cloudflare.com/ajax/libs/socket.io/4.0.1/socket.io.js"></script>
        <script src="https://code.jquery.com/jquery-3.6.0.min.js"></script>
    </head>
    <body>
        <h1>Online Chat Room</h1>
        <div class="chat-container">
            <div class="user-container">
                <input type="text" id="username" autocomplete="off" placeholder="Enter a username">
                <button onclick="setUsername()">Set Username</button>
            </div>
            <div id="messages"></div>
            <div class="message-container">
                <input type="text" id="myMessage" autocomplete="off" placeholder="Enter a message...">
                <button onclick="sendMessage()">Send</button>
            </div>
        </div>
        <script>
            var socket = io({ transports: ['websocket', 'polling', 'flashsocket'] });
            var usernameSet = false;
            socket.on('connect', function() {
                console.log("Connected to the server!");
                socket.on('message', function(msg){
                    $('#messages').append($('<div>').text(msg));
                    $('#messages').scrollTop($('#messages')[0].scrollHeight);
                });
            });
            function setUsername() {
                var username = $('#username').val();
                if (username) {
                    socket.emit('set_username', username);
                    usernameSet = true;  // Mark that username is set.
                } else {
                    alert("Username cannot be empty!");
                }
            }
            function sendMessage() {
                if (usernameSet) {
                    var message = $('#myMessage').val();
                    if (message) {
                        socket.send(message);
                        $('#myMessage').val('');
                    } else {
                        alert("Message cannot be empty!");
                    }
                } else {
                    alert("Please set a username first!");
                }
            }
        </script>
    </body>
    </html>
    '''
        # Save the template to a file
        with open('templates/index.html', 'w') as file:
            file.write(html_code)
        socketio.run(app, host='0.0.0.0', port=5000)
                      
  5. Press the Esc key, and then enter :wq to save the changes.

  6. Run the sudo python3 ECS02_ws.py command to run the script.

  7. When the following output is displayed, the WebSocket application has started on port 5000.

    Server initialized for threading.
     * Serving Flask app 'ECS02_ws' (lazy loading)
     * Environment: production
       WARNING: This is a development server. Do not use it in a production deployment.
       Use a production WSGI server instead.
     * Debug mode: off
     * Running on all addresses.
       WARNING: This is a development server. Do not use it in a production deployment.
     * Running on http://192.168.*.*:5000/ (Press CTRL+C to quit)
    

    If the application fails to start, check if the port is already in use or if you copied the commands or code incorrectly.

Step 2: Configure a server group

  1. Log on to the ALB console.

  2. In the top navigation bar, select the region where the instance is deployed.

  3. In the left-side navigation pane, click Server Group.

  4. Click Create Server Group. On the Create Server Group page, configure the following parameters, leave the others at their default values, and click Create.

    Parameter

    Description

    Server Group Type

    Select Server.

    VPC

    Select the VPC where the backend servers ECS01 and ECS02 are located.

    The backend servers must be in the same VPC as the ALB instance.

  5. In the The server group is created dialog box, click Add Backend Server.

  6. On the Add Backend Server page, select ECS01 and ECS02 and add them to the server group. Set their port to 5000, which is the port used by the WebSocket application in this tutorial.

Step 3: Configure an HTTP listener

  1. Log on to the ALB console.

  2. In the top navigation bar, select the region where the instance is deployed.

  3. In the left-side navigation pane, select Instances.

  4. On the Instances page, find the target instance, and then click Create Listener in the Actions column.

  5. On the Configure Listener page, configure the following parameters and leave the other parameters with their default values. Then, click Next.

    Parameter

    Description

    Select Listener Protocol

    Select HTTP.

    Listener Port

    Set the port to 5000.

  6. On the Server Group page, configure the following parameter and leave the other parameters with their default values. Then, click Next.

    Parameter

    Description

    Server Group

    Select the server group that you created.

  7. On the Confirm page, review the settings and click Submit. Then, wait for the listener to be created.

Step 4: Configure DNS resolution

For production, we recommend mapping a custom domain name to the ALB instance's domain name by creating a CNAME record.

  1. In the left-side navigation pane, choose ALB > Instances.

  2. On the Instances page, copy the DNS name of the created ALB instance.

  3. Follow these steps to add a CNAME record.

    Note

    If your domain name is registered by a provider other than Alibaba Cloud, you must add the domain name to the Alibaba Cloud DNS console before you can configure DNS resolution. For more information, see Manage domain names. If your domain name is registered with Alibaba Cloud, proceed with the following steps.

    1. Log on to the Alibaba Cloud DNS console.

    2. On the Authoritative DNS Resolution page, find the target domain name, and click Settings in the Operations column.

    3. On the Settings page, click Add Record.

    4. In the Add Record panel, configure the CNAME record by setting the following parameters, and then click OK.

      Parameter

      Description

      Record Type

      Select CNAME from the drop-down list.

      Hostname

      The prefix of your domain name. This tutorial uses @.

      Note

      To use a root domain, set the hostname to @.

      Query Source

      Select Default.

      Record Value

      Enter the DNS name of the ALB instance that you copied.

      TTL

      Time to live (TTL) is the duration that a DNS record is cached on a DNS server. Use the default value.

Step 5: Verify the result

Verify that ALB delivers real-time messages over WebSocket by opening the application in a browser on two separate computers and exchanging messages.

  1. In a browser, go to http://<your-domain-name>:5000 to access the online chat room application.

    The Online Chat Room interface loads. The message "You have entered the chat room!" appears in the chat message area, which indicates that the WebSocket connection is established. The page contains a username setting area with a Set Username button and a message sending area with a Send button.

    If you open your browser's developer tools, the Network tab shows that the browser is communicating using the WebSocket protocol.

    In the Network panel, set the filter to websocket. A WebSocket request with a status code of 101 indicates that the protocol upgrade was successful. The connection status is Pending, which means that the persistent WebSocket connection is established and active.

  2. Enter a username for the chat and click Set Username.

  3. On each computer, enter multiple chat messages and click Send to test the application.

    Both browsers receive the messages in real time.

    The Online Chat Room application is successfully deployed. The page contains a Username input box and a Set Username button, a chat message display area, and a message input box with a Send button at the bottom. Multiple users, such as User1 and User2, can have a real-time conversation in the chat room.

  4. This confirms that ALB provides real-time, high-availability messaging over WebSocket.

FAQ

How do I use WebSocket Secure?

WebSocket Secure is the encrypted version of the WebSocket protocol.

HTTPS listeners support WebSocket Secure by default. To use WebSocket Secure, select HTTPS when you configure the listener.

Am I charged for using WebSocket?

WebSocket and WebSocket Secure do not incur additional charges.

Which regions support WebSocket?

All regions that support ALB also support WebSocket and WebSocket Secure.

References

This tutorial demonstrates deploying Redis on an ECS instance for testing. However, a single Redis server is a potential single point of failure. For production environments, we recommend that you use Tair to ensure high availability. For more information, see Quick start for Tair.