All Products
Search
Document Center

Server Load Balancer:Use WebSocket to enable real-time messaging

Last Updated:Dec 09, 2024

WebSocket is a communication protocol that supports full-duplex communication over a single TCP connection. WebSocket is designed to establish two-way communication between clients and servers over persistent connections. WebSocket requires fewer overheads and supports lower network latency because connections are less frequently created or closed. Compared with the traditional request-response HTTP protocol, WebSocket supports higher efficiency for real-time interactions. WebSocket is suitable for scenarios that require real-time communication. Classic Load Balancer (CLB) supports WebSocket by default.

Introduction to WebSocket

Why WebSocket?

With the rapid development of Internet technologies, web applications become more diversified. Some scenarios, such as livestreaming chat and live comments, require server-side real-time push. In traditional solutions, the round-robin algorithm is used to initiate HTTP requests from client browsers to servers at a fixed interval, such as 1 second. Then, the servers return the latest data to the clients. However, this solution has a shortcoming. Clients must frequently initiate requests, which contain large HTTP headers but less useful information. The requests not only increases loads on servers but also cause a significant waste of bandwidth resources.

To address this issue, HTML5 adopts the WebSocket protocol, which establishes more efficient communication between clients and servers. WebSocket supports full-duplex communication that allows simultaneous, two-way communication between clients and servers. It enables servers to proactively push the latest data to clients without the need for client-side polling. With fewer unnecessary requests, two-way communication significantly improves the efficiency of data exchange and reduces the consumption of server and bandwidth resources while providing smoother real-time interactions for users.

WebSocket characteristics

Before a WebSocket connection can be established, the client and the server must complete a three-way TCP handshake and a special HTTP request also called handshake to upgrade HTTP connections to WebSocket connections. During the upgrade, the client and the server communicate over WebSocket instead of HTTP. Two-way communication can be established over the same WebSocket connection.

After a WebSocket connection is established, it remains open to allow two-way communication similar to sockets. WebSocket does not require a new connection or wait for a response for each round of data exchange. The persistent, low-latency connections established over WebSocket significantly improve the efficiency of data exchange.

image

WebSocket exchanges data between clients and servers as data frames. WebSocket messages require smaller headers and can be exchanged as text or binary data. This type of communication reduces overheads on persistent connections and improves the efficiency of data exchange. It requires less server and bandwidth resources while providing high-performance real-time interactions.

For more information about WebSocket, see The WebSocket Protocol.

Use scenarios

WebSocket is suitable for scenarios that require instant or real-time two-way communication, such as AI applications, online chatrooms, real-time notification systems, multi-play online games, and real-time message push.

Example

A company deployed an online chat application on Alibaba Cloud. Users can visit the domain name to access the backend servers, on which the users can interact with each other. The application requires instant communication to support low-latency, high-efficiency, two-way, and real-time data exchange between users.

High concurrency and persistent connection management become challenges. As the number of users increases, the traditional HTTP model can no longer support simultaneous, real-time communication for a large number of users. Each round of data exchange requires a new connection. This model greatly increases loads on the servers and decreases the server performance.

In this scenario, the company can use CLB together with WebSocket to manage persistent connections and maintain high concurrency. The company can deploy WebSocket applications on multiple backend servers. This solution ensures service high availability and enables reliable, efficient, and real-time message push for online chatrooms.

image

Usage notes

HTTP listeners of CLB support WebSocket by default. In addition, CLB supports rolling updates. Configuration changes do not affect existing persistent connections.

When you use CLB, take note of the following items:

  • If you want to establish a connection between CLB and a backend server over a specific version of HTTP, such as HTTP/1.1, we recommend that you use web servers that support the HTTP version as backend servers.

  • The default timeout period of HTTP listeners is 60 seconds. If CLB does not exchange data with a backend server for 60 seconds, the connection is closed.

    • You can change the value of the Connection Request Timeout parameter on the HTTP listener to set the timeout period to a desired value.

    • To maintain a connection, you must use a keepalive mechanism to exchange packets between ALB and the backend servers every 60 seconds.

Prerequisites

  • A CLB instance is created. For more information, see Create a CLB instance.

  • Three Elastic Compute Service (ECS) instances are created. In this example, the ECS instances are named ECS01, ECS02, and ECS03.

    • WebSocket applications are deployed on ECS01 and ECS02. Redis is deployed on ECS03.

    • In this example, all ECS instances use the CentOS 7.9 operating system.

    • We recommend that you add ECS01, ECS02, and ECS03 to the same security group. If you add the ECS instances to different security groups, you must allow access to the communication ports from each other.

  • A domain name is registered and an Internet Content Provider (ICP) number is obtained for the domain name. For more information, see Register a generic domain name and ICP filing process.

Procedure

Step 1: Deploy applications

Deploy Redis on ECS03 and WebSocket applications on ECS01 and ECS02.

The following example shows how to deploy a test online chatroom. In this example, the ECS instances use the CentOS 7.9 operating system. The example is for reference only. Adjust the configurations for your programs and applications.

Deploy Redis on ECS03

  1. Log on to ECS03.

  2. Run the following commands on ECS03 to deploy and configure Redis:

    # Install Extra Packages for Enterprise Linux (EPEL)
    sudo yum install epel-release -y
    
    # Install Redis
    sudo yum install redis -y
    
    # Start and enable Redis
    sudo systemctl start redis
    sudo systemctl enable redis
    
    # Check and modify 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 Redis to apply the configuration modifications
    sudo systemctl restart redis
    
    # Query whether Redis is running
    sudo systemctl status redis
    
  3. If commands do not report an error and the output shows that Redis is in the active (running) state, Redis is deployed and the configurations take effect, as shown in the following figure.

    image

Deploy a WebSocket application on ECS01

  1. Log on to ECS01.

  2. Run the sudo pip3 install flask flask-socketio flask-cors redis command to install the dependency library.

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

  4. Copy and paste the following code:

    Sample code for deploying a test application

    Note

    Replace the IP address in the redis_url field on line 13 with the IP address of the Redis server, which is the IP address of 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 to manage queues and store states
    redis_url = "redis://192.168.*.*:6379/0"  # Replace the IP address with the IP address of your Redis server
    redis_client = redis.StrictRedis.from_url(redis_url)
    
    # Add the DEBUG log level to facilitate 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  # Obtain the client-side session ID
            print(f"Session {session_id} connected.")
            welcome_message = "Welcome to the chatroom!"
            emit('message', welcome_message)
            set_session_data(session_id, "username", '')  # The initialized username is 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} left the chatroom."
                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"Set the username of the client {session_id} to {username}")
        emit('message', f"Your username is 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 the message. Specify a username first."
            emit('message', warning_message)
            print(warning_message)
    
    
    if __name__ == '__main__':
        # Store in the templates directory
        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>Chatroom</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 Chatroom</h1>
        <div class="chat-container">
            <div class="user-container">
                <input type="text" id="username" autocomplete="off" placeholder="Enter a username">
                <button onclick="setUsername()">Set as 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;  // Enter a username identifier
                } else {
                    alert("The username cannot be empty.");
                }
            }
            function sendMessage() {
                if (usernameSet) {
                    var message = $('#myMessage').val();
                    if (message) {
                        socket.send(message);
                        $('#myMessage').val('');
                    } else {
                        alert("The message cannot be empty.");
                    }
                } else {
                    alert("Specify a username first.");
                }
            }
        </script>
    </body>
    </html>
    '''
    
        # Save the template as 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 enter :wq to save the configurations.

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

  7. The following output indicates that the WebSocket application is enabled and uses 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 you fail to enable the WebSocket application, check whether the specified port is occupied by another application or errors exist in the commands or code.

Deploy a WebSocket application on ECS02

  1. Log on to ECS02.

  2. Run the sudo pip3 install flask flask-socketio flask-cors redis command to install the dependency library.

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

  4. Copy and paste the following code:

    Sample code for deploying a test application

    Note

    Replace the IP address in the redis_url field on line 13 with the IP address of the Redis server, which is the IP address of 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 to manage queues and store states
    redis_url = "redis://192.168.*.*:6379/0"  # Replace the IP address with the IP address of your Redis server
    redis_client = redis.StrictRedis.from_url(redis_url)
    
    # Add the DEBUG log level to facilitate 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  # Obtain the client-side session ID
            print(f"Session {session_id} connected.")
            welcome_message = "Welcome to the chatroom!"
            emit('message', welcome_message)
            set_session_data(session_id, "username", '')  # The initialized username is 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} left the chatroom."
                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"Set the username of the client {session_id} to {username}")
        emit('message', f"Your username is 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 the message. Specify a username first."
            emit('message', warning_message)
            print(warning_message)
    
    
    if __name__ == '__main__':
        # Store in the templates directory
        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>Chatroom</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 Chatroom</h1>
        <div class="chat-container">
            <div class="user-container">
                <input type="text" id="username" autocomplete="off" placeholder="Enter a username">
                <button onclick="setUsername()">Set as 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;  // Enter a username identifier
                } else {
                    alert("The username cannot be empty.");
                }
            }
            function sendMessage() {
                if (usernameSet) {
                    var message = $('#myMessage').val();
                    if (message) {
                        socket.send(message);
                        $('#myMessage').val('');
                    } else {
                        alert("The message cannot be empty.");
                    }
                } else {
                    alert("Specify a username first.");
                }
            }
        </script>
    </body>
    </html>
    '''
    
        # Save the template as 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 enter :wq to save the configurations.

  6. Run the sudo python3 ECS02_ws.p command to execute the script.

  7. The following output indicates that the WebSocket application is enabled and uses 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 you fail to enable the WebSocket application, check whether the specified port is occupied by another application or errors exist in the commands or code.

Step 2: Configure a server group

  1. Log on to the CLB console.

  2. In the top navigation bar, select the region in which the CLB instance is deployed.

  3. In the left-side navigation pane, click Instances. On the Instances page, click the ID of the CLB instance that you want to manage.

  4. On the vServer Groups tab, click Create vServer Group. On the Create vServer Group page, configure the parameters. The following table describes some key parameters. Other parameters use the default values. After you complete the configurations, click Create.

    Parameter

    Description

    vServer Group Name

    Enter the name of vServer Group RS1.

  5. On the vServer Groups tab, find the vServer group and click Modify in the Actions column.

  6. On the Modify vServer Group page, click Add. In the Servers panel, select ECS01 and ECS02 and set the port to the port used by the WebSocket applications. In this example, the WebSocket applications use port 5000.

    image

  7. On the Modify vServer Group page, select the backend servers that you add and click Save.

Step 3: Add an HTTP listener

  1. Log on to the CLB console.

  2. In the top navigation bar, select the region in which the CLB instance is deployed.

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

  4. On the Instances page, find the CLB instance and click Configure Listener in the Actions column.

  5. In the Protocol & Listener step, configure the parameters. The following table describes some of the parameters. Configure the other parameters based on your business requirements. After you configure the parameters, click Next.

    Parameter

    Description

    Select Listener Protocol

    Select HTTP.

    Listener Port

    In this example, port 5000 is used.

  6. In the Backend Servers step, configure the parameters. The following table describes some of the parameters. Configure the other parameters based on your business requirements. After you configure the parameters, click Next.

    Parameter

    Description

    Server Group

    Select the server group that you want to use.

  7. In the Health Check step, click Next. You can use the default parameter values or specify values based on your business requirements.

  8. In the Confirm step, check whether the parameters are correctly configured and click Submit.

Step 4: Add a DNS record

Note
  • For domains not registered on Alibaba Cloud, you must first add the domain name to Alibaba Cloud DNS before you can create a DNS record.

  • If your CLB instance is internal-facing, you must first associate an Elastic IP address (EIP) with it, then create an A record to map the domain name to the EIP for Internet access.

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

  2. On the Instances page, find the CLB instance that you want to manage and copy the Endpoint of the CLB instance.

  3. Perform the following steps to create an A record.

    1. Log on to the Alibaba Cloud DNS console.

    2. On the Authoritative DNS Resolution page, find the domain name that you want to manage, and click DNS Settings in the Actions column.

    3. On the DNS Settings page, click Add Record.

    4. In the Add Record panel, configure the following parameters, use the default values for all the other parameters or set them based on actual conditions, and click OK.

      Parameter

      Description

      Record Type

      Select A from the drop-down list.

      Hostname

      The prefix of your domain name. In this example, www is entered.

      Note

      If you use a root domain name, enter @.

      Record Value

      Enter the A address corresponding to your domain name. In this example, the IP address of your CLB instance is used.

Step 5: Verify the result

Prepare two computers that have different IP addresses and support Internet access. Send messages from browsers on the computers to test whether the CLB instance can push the WebSocket messages in real time.

  1. Visit http://Domain name:5000 from the browsers to access the online chatroom.

    The following figure shows that the chatroom is accessible.

    image

    If you open the developer tools on your browser, you can find that WebSocket communication is established on the Network tab.

    image

  2. Enter a username and click Set as Username.

  3. Enter a message and click Send. Repeat this operation on multiple computers.

    The following figure shows that messages from different computers are displayed on the browsers.

    image

  4. The preceding tests show that the CLB instance can push WebSocket messages in real time while maintaining high availability.

FAQ

How do I use the WebSocket Secure protocol?

WebSocket Secure is the encrypted version of WebSocket.

By default, HTTPS listeners support WebSocket Secure. To enable WebSocket Secure, create an HTTPS listener.

Am I charged for using WebSocket?

You are not charged for using WebSocket or WebSocket Secure.

Which regions support WebSocket?

WebSocket and WebSocket Secure are supported in all regions of CLB.

References

To facilitate the tests, this topic uses a simple example to describe how to deploy Redis on an ECS instance. However, Redis server errors may cause single points of failure (SPOFs). In your production environment, we recommend that you use What is Tair (Redis OSS-compatible)? to improve application high availability. For more information, see Overview.