Echtzeitanwendungen wie WebSockets werden jeden Tag verwendet, jetzt mehr denn je mit dem Aufkommen von Social-Media-Websites wie Twitter, Google+ und Facebook. Server pushen Updates an Clients, anstatt in regelmäßigen Abständen Änderungen abzufragen – dies wird als „WebSocket“ bezeichnet. Alles in allem war es ein fantastischer Fortschritt, aber es hat seine Nachteile, die sich für Python-Entwickler als problematisch erwiesen haben. Dieser Artikel hilft Ihnen beim Erstellen eines Websocket-Servers mit Python.

Mit den neuesten Entwicklungen der Python-Sprache und mit Hilfe von Frameworks wie Tornado ist die Handhabung von Websocket- oder HTTP/2-Verbindungen viel einfacher geworden. Tornado ist eine Python-Netzwerkbibliothek, die es Entwicklern ermöglicht, HTTP-Server in Python zu erstellen, und die ihnen die Möglichkeit bietet, WebSocket- und andere Netzwerkübertragungen einfacher zu bewältigen. Tornado bietet Webserver-Fähigkeiten in Python, die es erfahrenen Entwicklern ermöglichen, diese Verbindungen besser zu handhaben.

Dieser Artikel veranschaulicht die Konzepte der asynchronen Programmierung und der Handhabung mehrerer Verbindungen. Wir zeigen Ihnen, wie Sie einen einfachen Chatroom-Server einrichten und mehreren Clients erlauben, sich mit ihm zu verbinden.

Was sind Websockets?

WebSocket ist ein Kommunikationsprotokoll, das Vollduplex-Kommunikation über eine einzelne TCP-Verbindung bereitstellt. WebSocket ist eine Kombination von Technologien, die eine vollständige bidirektionale Kommunikation zwischen einem Browser und einem Server ermöglicht. Dies erleichtert Datenübertragungen in Echtzeit, da Sie die Client- und Serveraktivitäten ohne Verzögerung gleichzeitig steuern können. Es hat einen geringeren Overhead als Halbduplex-Alternativen wie HTTP-Polling und erleichtert die Datenübertragung in Echtzeit vom und zum Server.

Die geringe Latenz und Multidirektionalität von WebSockets sind besonders nützlich für Anwendungen, die Nachrichten senden oder empfangen müssen, während sie Antworten vom Server erhalten. Da WebSockets mit einer HTTP-Anforderung beginnen, sind sie zu 100 % mit der vorhandenen Webinfrastruktur kompatibel und können sogar auf demselben Port ausgeführt werden wie Ihre vorhandenen Webanforderungen, z. B. diejenigen, die über die Ports 80 oder 443 (zum Beispiel) gestellt werden. Diese Kompatibilität vereinfacht die ursprungsübergreifende Sicherheit, was bedeutet, dass niemand in der client- oder serverseitigen Infrastruktur seine Netzwerke ändern muss, um eine sichere Kommunikation mit WebSockets zu unterstützen.

Voraussetzungen

Wenn Sie Ihrer Anwendung WebSocket-Unterstützung hinzufügen, müssen Sie einen Webserver wie Tornado verwenden, der über eine integrierte WebSocket-Unterstützung verfügt.

Tornado wird mit einer eigenen Implementierung von WebSocket geliefert, die beim Erstellen eines Websocket-Servers mit Python hilft. Für die Zwecke dieses Artikels ist dies genau das, was wir brauchen. Wenn Sie keine Erfahrung und keine Ahnung mit diesem Zeug haben, ist es vielleicht keine gute Idee, das Rad neu zu erfinden. Stattdessen wäre es der beste Ansatz für Sie, mit einem funktionierenden Beispiel zu beginnen und es zu verstehen, und genau das werden wir hier tun!

Der Code verwendet das Konzept von WebSockets und der Tornado-Netzwerkbibliothek unter Verwendung der Python-Sprache, und das ist alles, was wir brauchen. Wir können WebSockets und Tornado mit den folgenden Pip-Befehlen installieren:

pip install tornado
pip install websockets

Einrichten der main.py-Datei

Der Chatroom-Server und der Chatroom-Client sind der Einfachheit halber zusammen in einer einzigen Datei implementiert. Erstellen Sie zunächst eine Datei main.py und importieren Sie die erforderlichen Module, um einen Websocket-Server mit Python zu erstellen:

#main.py

import tornado.escape  #for escaping/unescaping methods for HTML, JSON, URLs, etc
import tornado.ioloop  #event loop for non-blocking sockets
import tornado.options  #command line parsing module to define options
import tornado.web  #provides a simple web framework with asynchronous featuresfrom tornado.options import define, options
import tornado.websocket  #implementation of the WebSocket protocol and bidirectional communication
import logging
import os.path
import uuid

from tornado.options import define, options

Definieren der restlichen Funktionen, einschließlich der Kundenseite. Alle Dinge, die Sie möglicherweise wissen müssen, sind im unten angegebenen Code vordefiniert:

define("port", default=8080, type=int)  #setting the port to 8000 which can easily be changed


class Application(tornado.web.Application):  #setting a tornado class as a web application
    def __init__(self):
        handlers = [(r"/", MainHandler), (r"/chatsocket", ChatSocketHandler)] #setting the nesassary urls
        settings = dict(
            cookie_secret="__TODO:_GENERATE_YOUR_OWN_RANDOM_VALUE_HERE__",
            template_path=os.path.join(os.path.dirname(__file__), "templates"),  #providing the templates path
            static_path=os.path.join(os.path.dirname(__file__), "static"),  #providing the static folder's path
            xsrf_cookies=True,
        )
        super().__init__(handlers, **settings)


class MainHandler(tornado.web.RequestHandler):
    def get(self):
        self.render("index.html", messages=ChatSocketHandler.cache)

Definieren des WebSocket-Handshakes

class ChatSocketHandler(tornado.websocket.WebSocketHandler):  #creating our main websocket class
    waiters = set()  #set number of joinable users
    cache = []
    cache_size = 200

    def get_compression_options(self):
        #non-none enables compression with default options.
        return {}

    def open(self):  #accepts a connection request and stores the parameters, a socket object for that user
        ChatSocketHandler.waiters.add(self)

    def on_close(self):  #removes the user by removing the object
        ChatSocketHandler.waiters.remove(self)

    @classmethod 
    def update_cache(cls, chat):  #Maintains a list of chat for broadcasting the messages
        cls.cache.append(chat)
        if len(cls.cache) > cls.cache_size:
            cls.cache = cls.cache[-cls.cache_size :]

    @classmethod 
    def send_updates(cls, chat):  #mange sending messages
        logging.info("sending message to %d waiters", len(cls.waiters))
        for waiter in cls.waiters:
            try:
                waiter.write_message(chat)  #outputting the messages
            except:  #except, in case of any errors
                logging.error("Error sending message", exc_info=True)

    def on_message(self, message):
        logging.info("got message %r", message)
        parsed = tornado.escape.json_decode(message)
        chat = {"id": str(uuid.uuid4()), "body": parsed["body"]}
        chat["html"] = tornado.escape.to_basestring(
            self.render_string("message.html", message=chat)
        )

        ChatSocketHandler.update_cache(chat)
        ChatSocketHandler.send_updates(chat)

Das WebSocket-Protokoll definiert zwei spezielle Arten von Übertragungen: den WebSocket-Handshake (send_updates) und die WebSocket-Nachricht (on_message). Der WebSocket-Handshake wird verwendet, um eine Verbindung zwischen Client und Server herzustellen, während die WebSocket-Nachricht gesendet wird, nachdem die Verbindung hergestellt wurde.

def main():  #and to close up everything
    tornado.options.parse_command_line()
    app = Application()
    app.listen(options.port)
    tornado.ioloop.IOLoop.current().start()


if __name__ == "__main__":
    main()

Schließen Sie schließlich die Hauptfunktion (die die WebSockets enthält).

Vorlagen einrichten

Nachdem wir die obige Datei erstellt haben, müssen wir nun die Vorlage und die statischen Dateien erstellen. Wie zuvor definiert, müssen wir zuerst die Ordner erstellen, in denen diese Dateien gespeichert werden, die „Vorlagen“ und „Statisch“ heißen würden. Wir können damit beginnen, die Vorlagen wie gezeigt zu erstellen:

#templates/index.html

<!DOCTYPE html>
<html>
  <head>
    <meta charset="UTF-8">
    <title>Tornado Chat Demo</title>
        <link href="https://cdn.jsdelivr.net/npm/[email protected]/dist/css/bootstrap.min.css" rel="stylesheet" integrity="sha384-1BmE4kWBq78iYhFldvKuhfTAU6auU8tT94WrHftjDbrCEXSU1oBoqyl2QvZ6jIW3" crossorigin="anonymous">
  </head>
  <body>
    <div id="body">
      <div id="inbox">
        {% for message in messages %}
          {% include "message.html" %}
        {% end %}
      </div>
      <div id="input">
        <form action="/a/message/new" method="post" id="messageform">
          <table>
            <tr>
              <td><input type="text" name="body" id="message" style="width:500px"></td>
              <td style="padding-left:5px">
                <input type="submit" value="{{ _("Post") }}">
                <input type="hidden" name="next" value="{{ request.path }}">
                {% module xsrf_form_html() %}
              </td>
            </tr>
          </table>
        </form>
      </div>
    </div>
    <script src="https://ajax.googleapis.com/ajax/libs/jquery/3.1.0/jquery.min.js" type="text/javascript"></script>
    <script src="{{ static_url("chat.js") }}" type="text/javascript"></script>
  </body>
</html>

Und jetzt die statischen Dateien:

#static/chat.js

$(document).ready(function() {
    if (!window.console) window.console = {};
    if (!window.console.log) window.console.log = function() {};

    $("#messageform").on("submit", function() {
        newMessage($(this));
        return false;
    });
    $("#messageform").on("keypress", function(e) {
        if (e.keyCode == 13) {
            newMessage($(this));
            return false;
        }
    });
    $("#message").select();
    updater.start();
});

function newMessage(form) {
    var message = form.formToDict();
    updater.socket.send(JSON.stringify(message));
    form.find("input[type=text]").val("").select();
}

jQuery.fn.formToDict = function() {
    var fields = this.serializeArray();
    var json = {}
    for (var i = 0; i < fields.length; i++) {
        json[fields[i].name] = fields[i].value;
    }
    if (json.next) delete json.next;
    return json;
};

var updater = {
    socket: null,

    start: function() {
        var url = "ws://" + location.host + "/chatsocket";
        updater.socket = new WebSocket(url);
        updater.socket.onmessage = function(event) {
            updater.showMessage(JSON.parse(event.data));
        }
    },

    showMessage: function(message) {
        var existing = $("#m" + message.id);
        if (existing.length > 0) return;
        var node = $(message.html);
        node.hide();
        $("#inbox").append(node);
        node.slideDown();
    }
};

Erstellen Sie außerdem eine message.html-Datei, die einen Teil des zusätzlichen js-Codes enthält.

#template/message.html

<div class="message" id="m{{ message["id"] }}">{% module linkify(message["body"]) %}</div>

Und es ist alles bereit!

Ausgabe 

Führen Sie das Projekt aus, indem Sie den Namen der Datei im Terminal ausführen, aber es wird keine Antwort ausgegeben, es sei denn, es liegt ein Fehler vor. Wir müssen zu dem Port gehen, den wir zuvor definiert haben, nämlich 8080. Öffnen Sie diesen Link http://127.0.0.1:8080/ und Sie sollten so etwas sehen:

Sie können versuchen, einen anderen Browser zu öffnen, um zu sehen, ob es funktioniert oder nicht, und nachdem Sie etwas gepostet haben, sollten Sie auch die Antworten auf Ihrem Terminal ausgedruckt sehen:

Um seine asynchronen Fähigkeiten zu überprüfen oder was wir tatsächlich mit WebSockets gemacht haben, können Sie seine Netzwerkleistung mit jedem einfachen alten Browser überprüfen:

Letzte Worte

Und das war’s, in diesem Artikel haben wir gelernt, wie man einen Websocket-Server mit Python und der Tornado-Netzwerkbibliothek erstellt. Jetzt gibt es hier viel Raum für Verbesserungen, z. B. bei der Authentifizierung. Es gibt auch viele andere Bibliotheken, die Sie ebenfalls verwenden können, aber Tornado ist bei weitem die anfängerfreundlichste und verfügt über eine Dokumentationsbibliothek. Wenn Sie weiter recherchieren möchten, ist dies der beste Ort.

Sie können sich auch auf diese Dateistruktur beziehen, wenn Sie sich verlaufen:

│   main.py
│   playbook.yml
│
├───static
│       chat.js
│
└───templates
        index.html
        message.html

Hier sind einige nützliche Tutorials, die Sie lesen können: