Online Chat--Introduction and Realization of WebSocket

Posted by phwae on Mon, 28 Feb 2022 18:41:14 +0100

Before HTML5, browsers and servers communicated through the HTTP protocol, so what's the use of introducing a WebSocket? Imagine a web page broadcasting a score of a football match live in text. Without a WebSocket, the client, or browser, usually updates the score dynamically in two ways: AJAX and long polling.

What is the difference between the two technologies? Take the following example (S for server and C for client):

Ajax mode:

C: Tell me the latest score
S: The score hasn't been updated yet

C: (5 Seconds later) Tell me the latest score
S: The score hasn't been updated yet

C: (5 Seconds later) Tell me the latest score
S: The score hasn't been updated yet
...

C: (5 Seconds later) Tell me the latest score
S: 2-1
(C Receive score, show score)

C: (5 Seconds later) Tell me the latest score
...(n Next)
S: 3-1
(C Receive score, show score)

Long polling:

C: Tell me the latest score
S: The score hasn't been updated yet. Wait until it's updated to tell you
...

S: (5 Minutes later) Latest score: 2-1
(C Receive score, show score)

C: Tell me the latest score
S: The score hasn't been updated yet. Wait until it's updated to tell you
...

S: (3 Minutes later) Latest score: 3-1
(C Receive score, show score)C: Tell me the latest score
S: The score hasn't been updated yet. Wait until it's updated to tell you
...

S: (5 Minutes later) Latest score: 2-1
(C Receive score, show score)

C: Tell me the latest score
S: The score hasn't been updated yet. Wait until it's updated to tell you
...

S: (3 Minutes later) Latest score: 3-1
(C Receive score, show score)

Ajax is a simple way to let the browser send an Ajax request at regular intervals, and the server returns it as soon as it receives the request, but it does not necessarily return valid data. The client sends the request until it gets a valid score and then updates the page. With long polling, instead of returning immediately after sending a request, the server is blocked there until the score is updated.

These two approaches have the following common points:

  • Requests are initiated by the client
  • Requests are made using the HTTP protocol

This is also an insufficiency of the HTTP protocol. Clients always contact the server, and the server cannot actively contact the client.

Difference:

  • For Ajax requests, the client sends the request until it gets a response.
  • Long polling, where clients block requests until they get a response.

The WebSocket protocol makes up for this shortage. With a Web ocket, the server can actively contact the client and send messages to it. The precondition is that the client and server must establish a connection first, because WebSocket is based on TCP protocol, and its handshake process uses HTTP protocol.

Communication using WebSocket s:

C: I need to build Websocket Connect. Services Required: ... Websocket Edition: ...
S: okļ¼ŒConfirmed and established connection
C: The score has been updated to tell me
S: ok

S: Latest Score: 2-1
(C Receive score, show score)
S: Latest Score: 3-1
(C Receive score, show score)

Using a WebSocket requires only one connection and the client receives the latest score without sending a request.

2. Realization

Install several third-party libraries before implementing the back-end code:

sudo pip3 install flask-sockets gunicorn redis

Backend support for WebSockets is implemented using the Flask-Sockets library, which supports registering a blueprint for WebSockets.

Create blueprints

Now we'll create a new WS under the / handlers directory. Py file and create a blueprint with the same name:

from flask import Blueprint

ws = Blueprint('ws', __name__, url_prefix='/ws')

In/handlers/init. Introduce in the PY file and create a list:

from .front import front
from .course import course
from .live import live
from .admin import admin
from .ws import ws


bp_list = [front, course, live, admin, ws]

Then in simpledu/app. Register in py file:

from simpledu.handlers import bp_list,ws

def register_blueprints(app):
    """Registration blueprint
    """

    for bp in bp_list:
        app.register_blueprint(bp)

Create Sockets instances and register blueprints

Next, on / app. Register the socket object in the PY file, which is an instance of creating the Sockets class:

from flask_sockets import Sockets        # Add Code


def register_blueprints(app):
    """Registration blueprint
    """

    for bp in bp_list:
        app.register_blueprint(bp)

    sockets = Sockets(app)                    # Add Code
    sockets.register_blueprint(ws)    # Add Code

To avoid blocking the Flask primary service to improve chat server performance, the server-side implementation is based on gevent, a third-party library that supports asynchronous I/O.

After introducing gevent, we need to consider how to synchronize messages. Here we use Redis's Publish Subscription system to make a simple message queue. Subscribers subscribe to channels, publishers publish messages to channels, and channels receive and receive message queues.

The terminal executes the following command to start the Redis service:

sudo service redis-server start

The backend first implements a Chatroom class, which is mainly used to manage client-channel connections and send messages to all clients, then starts the chat room asynchronously with gevent. It then implements an interface (view function) provided by Flask-Sockets to join the client to Redis's chat room channel, receive messages from the client, and publish them to Redis's chat room channel.

Write the following code to/handlers/ws.py file:

import redis
import gevent
from flask import Blueprint


ws = Blueprint('ws', __name__, url_prefix='/ws')

# This object is a Redis client that can either publish messages to or subscribe to channels
redis = redis.from_url('redis://127.0.0.1:6379')


class Chatroom:

    def __init__(self):
        self.clients = []               # User List
        self.pubsub = redis.pubsub()    # Initialize Publish Subscription System
        self.pubsub.subscribe('chat')   # Subscribe to chat channels

    def register(self, client):
        """Register users and add them to the user list

        :para client: geventwebsocket.websocket.WebSocket Instances of classes
        """
        self.clients.append(client)

    def send(self, client, data):
        """Send data to browser

        :para client: geventwebsocket.websocket.WebSocket Instances of classes
        :para data: To send a message, binary dictionary string b'{"user": xxx}'
        """
        # Call client's send method to send message to browser
        # If an exception occurs, the connection is closed and the client is removed
        try:
            client.send(data.decode())
        except:
            self.clients.remove(client)

    def run(self):
        # Publish the subscription system object self in the next line of code. listen method of pubsubis blocked
        # Redis's publishing subscription system is used here
        # Just because it blocks listening and Message Queuing ensures that messages move in FIFO order
        # When the user enters information on the browser page and clicks the Speak button
        # Browser calls inbox object to send data to server
        # The server's view function calls geventwebsocket.websocket.WebSocket class
        # Receive method of instance to receive data
        # The redis client then calls the publish method to send this data to the chat channel
        # Receive message here, enter for loop
        for message in self.pubsub.listen():
            if message['type'] == 'message':
                # To send data to the browser, the binary dictionary string b'{'user': xxx}'
                data = message.get('data')
                # Send data to all users in the user list
                # This is geventwebsocket.websocket.WebSocket instance sends data to browser
                for client in self.clients:
                    # It takes a while to send a message, here it is sent asynchronously using gevent
                    gevent.spawn(self.send, client, data)

    def start(self):
        # Because self. The run method will block the run, using asynchronous execution here
        gevent.spawn(self.run)


chat = Chatroom()   # Initialize chat room objects
chat.start()        # Start chat room asynchronously

Some students may wonder why a start function has been defined in the Chatroom class. Written in the comment, it is used to execute the run function asynchronously. In a real company project, such WebSocket services are usually managed independently as a separate service or directly using the products of the company that does them. In our small project, the WebSocket service is embedded within the Web service. If the run function is not executed asynchronously, the Web service will be blocked and page requests will not be responded to.

Next to the code above, continue at / handlers/ws. Add a processing function to py. Our chat function actually establishes a WebSocket connection for each client, which is actually a browser. Write the following code into it:

@ws.route('/send')
def inbox(ws):
    # Register users, that is, put ws in the clients list in the chat room
    chat.register(ws)
    # With Flask-Sockets, the ws connection object is automatically injected into the routing handler, which handles messages sent from the front end.
    # Notice the while loop below, where the receive() function is actually blocking until the front end sends a message.
    # Messages are placed on the chat channel, our message queue, and are looped until the websocket connection is closed.
    while not ws.closed:
        message = ws.receive()
        if message:
            # Send message to chat channel
            redis.publish('chat', message)

Flask-Sockets itself is extremely simple in code, creating a socket class, making its instance an object equivalent to the application object, and applying the object's wsgi_ Change the app property to an instance of the SocketMiddleware class.

The rest of the work is done by dependent libraries. Creating a server-like object ws that follows the WebSocket protocol is accomplished by the GeventWebSocket library. Processing instant messages from the front end is done by the message queue of the PUB/SUB system of the Redis server; The handler is blocked, and the work of executing code asynchronously is done by Gevent; The final multi-process asynchronous startup application is done by Gunicorn.

Topics: Python Ajax server Flask websocket