WebSockets

Inhaltsübersicht

  • Synchrone - Asynchrone Kommunikation
  • Einführung in WebSockets
    • WebSocket Protokoll
    • Opening Handshake
  • WebSocket Client
  • WebSocket mit Socket.IO
    • WebSocket mit Socket.IO und Flask
    • Exkurs: Coroutines
    • Beispiel in 3 Schritten
  • websocket
    • Beispiel
  • Tornado
    • Tornado.websocket
  • Web Application Messaging Protocol (WAMP)
    • Autobahn
    • Twisted
    • asyncio
    • Crossbar.io

Leistungsnachweis "WebSockets"

Installieren sie die drei Frameworks

mit ihren jeweiligen Abhängigkeiten und entwickeln sie jeweils die Aufgabenstellung vergleichbar zu der unten aufgeführten Beispiel-Aufgabe Polling mit Socket.IO

Vergleichen sie sowohl die Installation als auch die Benutzung der drei Frameworks mit ihren Stärken und Schwächen insbesondere bezgl. ihrer Interfaces und versuchen sie eine persönliche Empfehlung daraus zu erstellen.

Erstellen sie ihren Vergleich sowie den von ihnen erstellten Code in einem IPython Notebook und geben dieses zusammen mit dem gesamten lauffähigen Code als Archiv ab.

Synchrone Kommunikation

Beispiele:

  • Telefonie
  • Telnet
  • Videokonferenz
  • Chat
  • Instant Messaging
  • ...

Asynchrone Kommunikation

Beispiele:

  • E-Mail
  • Newsgroup
  • Forum
  • Mailing-Listen
  • RSS-Feeds
  • ...

Einführung in WebSockets

WebSocket Protokoll

The WebSocket Protocol - RFC 6455

Das WebSocket-Protokoll ermöglicht die bidirektionale Kommunikation zwischen einem Client - auf dem ein nicht vertrauenswürdiger Code in einer kontrollierten Umgebung läuft - zu einem entfernten Host, der die Kommunikation mit diesem Code erlaubt hat. Das dafür verwendete Sicherheitsmodell ist das von den Web-Browsern normalerweise verwendete quell-basierte Sicherheitsmodell. Das auf dem TCP-Protokoll aufbauende Protokoll besteht aus einem Start-Handshake auf das elementare Nachrichtenrahmen folgen. Das Ziel dieser Technologie ist es, einen Mechanismus erschaffen, der browserbasierten Anwendungen, die mit Servern eine zwei-Wege-Kommunikation benötigen, dies ohne die Abhängigkeit von mehreren offenen HTTP-Verbindungen zu ermöglichen (wie z.B. die Benutzung von XMLHttpRequest oder <iframe>s oder long polling Mechanismen).

Opening Handshake

Client Request

GET /chat HTTP/1.1
Host: server.example.com
Upgrade: websocket
Connection: Upgrade
Sec-WebSocket-Key: dGhlIHNhbXBsZSBub25jZQ==
Origin: http://example.com
Sec-WebSocket-Protocol: chat, superchat
Sec-WebSocket-Version: 13

Server Response

HTTP/1.1 101 Switching Protocols
Upgrade: websocket
Connection: Upgrade
Sec-WebSocket-Accept: s3pPLMBiTxaQ9kYGzzhZRbK+xOo=

WebSocket Client

Quelle: WebSocket and Socket.IO von David Walsh

In [1]:
%%javascript
// Create a socket instance
var socket = new WebSocket('ws://localhost:8080');

// Open the socket
   socket.onopen = function(event) {

    // Send an initial message
    socket.send('I am the client and I\'m listening!');

    // Listen for messages
    socket.onmessage = function(event) {
        console.log('Client received a message',event);
    };

    // Listen for socket closes
    socket.onclose = function(event) {
        console.log('Client notified socket has closed',event);
    };

    // To close the socket....
    //socket.close()

};

WebSocket mit Socket.IO

Quelle: WebSocket and Socket.IO von David Walsh

In [ ]:
%%javascript
// Create SocketIO instance, connect
var socket = new io.Socket('localhost',{
    port: 8080
});
socket.connect(); 

// Add a connect listener
socket.on('connect',function() {
    console.log('Client has connected to the server!');
});
// Add a connect listener
socket.on('message',function(data) {
    console.log('Received a message from the server!',data);
});
// Add a disconnect listener
socket.on('disconnect',function() {
    console.log('The client has disconnected!');
});

// Sends a message to the server via sockets
function sendMessageToServer(message) {
    socket.send(message);
};

WebSocket mit Socket.IO und Flask

Quelle: Talk at SF Python 2015 by Matthew Makai mit dem entsprechenden (und überarbeitenden) Code in Github bzw. im Youtube Video

Voraussetzungen:

  • In-Memory Datenbank Redis, einer hoch-performanten Schlüssel-Werte-Datenbank mit dem Python Wrapper redis
  • gevent als einer auf Coroutinen aufgebauten Netzwerk-Bibliothek, gevent baut dabei sowohl auf libev, einer hoch-performanten Event-Loop-Bibliothek, als auch auf greenlet, einer Bibliothek für eine leichtgewichtige nebenläufige Programmierung, auf.

Exkurs: Coroutines

Coroutines ermöglichen das asynchrone Programmieren in Python. Coroutines benutzen das Python Schlüsselwort yield analog wie bei Generators (s. Python Tips) um die Code-Ausführung anzuhalten oder fortzusetzen, anstelle z.B. einer Kette von Callback-Funktionen.

Siehe das Beispiel Coroutines in Python Tips in den folgenden Seiten.

Mehr Beispiele und Informationen sind zudem in dem Vortrag von David Beazley Coroutines and Concurrency enthalten.

In [3]:
def grep(pattern):
    print("Searching for", pattern)
    while True:
        line = (yield)
        if pattern in line:
            print(line)
In [4]:
search = grep('coroutine')
In [5]:
next(search)
('Searching for', 'coroutine')
In [6]:
search.send("I love you")
In [7]:
search.send("Don't you love me?")
In [8]:
search.send("I love coroutines instead!")
I love coroutines instead!
In [9]:
search.close()

1. Schritt - Flask ohne Socket.IO

In [10]:
%%writefile app.py
import redis
from flask import Flask, render_template, request

app = Flask(__name__)
db = redis.StrictRedis('localhost', 6379, 0)


@app.route('/')
def main():
    c = db.incr('counter')
    return render_template('main.html', counter=c)

if __name__ == '__main__':
    app.run(debug=True,host="0.0.0.0",port=5000)
Writing app.py
In [11]:
%%writefile templates/main.html
<html>
<head><title>WebSocket Introduction</title></head>
<body>
    <h1>Welcome to the WWW of 1995!</h1>
    <br />
    <h2>{{ counter }}</h2> 
</body>
</html>
Writing templates/main.html

2. Schritt - Flask mit Socket.IO

In [12]:
%%writefile app.py
from gevent import monkey
monkey.patch_all()

import cgi
import redis
from flask import Flask, render_template, request
from flask_socketio import SocketIO

app = Flask(__name__)
db = redis.StrictRedis('localhost', 6379, 0)
socketio = SocketIO(app)

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

@socketio.on('connect', namespace='/wsintro')
def ws_conn():
    c = db.incr('user_count')
    socketio.emit('msg', {'count': c}, namespace='/wsintro')

@socketio.on('disconnect', namespace='/wsintro')
def ws_disconn():
    c = db.decr('user_count')
    socketio.emit('msg', {'count': c}, namespace='/wsintro')

if __name__ == '__main__':
    socketio.run(app,host="0.0.0.0",port=5000)
Overwriting app.py
In [13]:
%%writefile templates/main.html
<html>
<head><title>WebSocket Introduction</title></head>
<body>
    <h1>Welcome to the WWW of 1995!</h1>
    <br />
    <h2><span id="user-count">{{ connected }}</span> users are currently using this page.</h2> 

    <script type="text/javascript" 
                src="http://code.jquery.com/jquery-1.11.1.min.js"></script>
    <script type="text/javascript" src="https://cdnjs.cloudflare.com/ajax/libs/socket.io/1.4.6/socket.io.min.js"></script>

    <script type="text/javascript">
        $(document).ready(function() {
            var url = "http://" + document.domain + ":" + location.port;
            var socket = io.connect(url + "/wsintro");
            socket.on('msg', function(msg) {
                $("#user-count").html(msg.count);
            });
        });
    </script>
    
</body>
</html>
Overwriting templates/main.html

3. Schritt - Polling mit Socket.IO

Die Javascript Client Bibliothek Socket.IO enthält m.E. in der Funktion emit einen Character-Encoding-Decoding Fehler, weswegen in dem folgenden Beispiel die Eingabe von Orten mit Umlauten mit einem BAD REQUEST Fehler zurückgeliefert wird. Der Fehler liegt nicht an der serverseitigen Flask-SocketIO Implementierung, da mit der Eingabe des richtigen Umlaut-Characters in der Browser-Entwicklungsumgebung der Request korrekt abgearbeitet wird.

In [14]:
%%writefile app.py
from gevent import monkey
monkey.patch_all()

import cgi
import redis
from flask import Flask, render_template, request
from flask_socketio import SocketIO

app = Flask(__name__)
db = redis.StrictRedis('localhost', 6379, 0)
socketio = SocketIO(app)

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

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

@socketio.on('connect', namespace='/wsintro')
def ws_conn():
    c = db.incr('user_count')
    socketio.emit('msg', {'count': c}, namespace='/wsintro')

@socketio.on('disconnect', namespace='/wsintro')
def ws_disconn():
    c = db.decr('user_count')
    socketio.emit('msg', {'count': c}, namespace='/wsintro')

@socketio.on('city', namespace='/wsintro')
def ws_city(message):
    print(message['city'])
    socketio.emit('city', {'city': message['city']},
                  namespace="/wsintro")

if __name__ == '__main__':
    socketio.run(app,host="0.0.0.0",port=5000)
Overwriting app.py
In [15]:
%%writefile templates/pymeetups.html
<!DOCTYPE html>
<html lang="de">
<head>
    <title>WebSocket Einführung - Python Groups</title>
    <meta charset="utf-8"/>
</head>
<body>
    <h1>In what cities other than München have you gone to Python meetups?</h1>
    <form method="post" action="#" id="cityform">
        <label>City</label>
        <input type="text" id="city"></input>
        <input type="submit" value="Submit"></input>
    </form>
    <div id="cities-list"></div>

    <script type="text/javascript" 
                src="http://code.jquery.com/jquery-1.11.1.min.js"></script>
    <script type="text/javascript" src="https://cdnjs.cloudflare.com/ajax/libs/socket.io/1.4.6/socket.io.min.js"></script>

    <script type="text/javascript" charset="utf-8">
        $(document).ready(function() {
            var url = "http://" + document.domain + ":" + location.port;
            var socket = io.connect(url + "/wsintro");
            $("#cityform").submit(function(event) {
                var city = $('#city').val();
                socket.emit('city', {'city': city});
                $('#city').val('');
                return false;
            });
            socket.on('city', function(msg) {
                $("#cities-list").prepend('<h3>' + msg.city + '<h3>');
            });
            
        });
    </script>
</body>
</html>
Writing templates/pymeetups.html

websockets

websockets ist eine reine auf asyncio basierte Python3-Bibliothek mit dem Fokus einer korrekten aber einfachen Implementierung des RFC 6455.

In [16]:
%%writefile websockets_server.py
#!/usr/bin/env python

import asyncio
import datetime
import websockets

async def time(websocket, path):
    while True:
        now = datetime.datetime.now().strftime("%d.%m.%Y %H:%M:%S")
        try:
            await asyncio.sleep(1)
            await websocket.send(now)
        except websockets.exceptions.ConnectionClosed as e:
            pass    
            
start_server = websockets.serve(time, '0.0.0.0', 8765)

asyncio.get_event_loop().run_until_complete(start_server)
asyncio.get_event_loop().run_forever()
Overwriting websockets_server.py
In [17]:
%%writefile websockets_client.html
<!DOCTYPE html>
<html>
    <head>
        <title>WebSocket demo</title>
    </head>
    <body>
        <script>
            var ws = new WebSocket("ws://192.168.155.101:8765/");
            ws.onmessage = function (event) {
                var message = document.getElementById("time");
                message.innerHTML = event.data;
            };
        </script>
        <h1>Uhrzeit vom Server</h1>
        <p id="time">uhrzeit</p1>
    </body>
</html>
Overwriting websockets_client.html

Tornado

Tornado ist ein Python Web Framework einschliesslich einer asynchronen Netzwerk Bibliothek. Mit der Nutzung von nicht-blockierenden Netzwerk-I/Os kann Tornado bis zu zehntausenden von offenen Verbindungen skalieren, sodass es für long polling Mechanismen, WebSockets, und anderen Anwendungen, die eine langlebige Verbindung für jeden Benutzer benötigen.

Web Application Messaging Protocol

Internet Draft The Web Application Messaging Protocol

WAMP ist ein Routing-Protokoll welches zwei Nachrichten-Pattern zur Verfügung stellt: Publish & Subscribe und geroutete Remote Procedure Calls. Es ist für die Verbindung von Komponenten in verteilten Anwendungen vorgesehen. WAMP benutzt WebSocket als voreingestelltes Transportprotokoll, kann aber über jedes andere Protokoll gesendet werden, welches geordnete, zuverlässige, bidirektionale und nachrichten-orientierte Kommunikation zur Verfügung stellt.

Remote Procedure Call

  • erlaubt den Aufruf einer Funktion von einem Code auf einem entferten Host über das Websocket-Protokoll,
  • asynchron, schnell, sauber, leicht - trotzdem einfach,
  • Argumente und Rückgabewerte können Arrays, Mappings, Zahlen und Zeichenketten sein,
  • arbeitet mit unterschiedlichen Programmsprachen,
  • arbeitet zwischen lokalen Prozessen genauso wie über das Internet

Publish & Subscribe

  • Ein Client meldet sich zu einem Thema an (SUBscribes)
  • Ein anderer Client verbreitet eine Nachricht zu diesem Thema (PUBlishes)
  • Alle zu diesem Thema angemeldeten Clients erhalten diese Nachricht

Dieser Mechanismus ist ähnlich wie RPC, mit dem Unterschied, dass mit Publish & Subscribe Nachrichten an 0 bis N Clients geschickt werden können und nicht nur an einen Client. Zudem gibt es keine Möglichkeit einen Rückgabewert zu erhalten.

Autobahn

Das Autobahn Projekt stellt verschiedene Open-Source Implementierungen (z.B. Autobahn|Python, Autobahn|JS) des WebSocket-Protokolls und des Web Application Messaging Protocol (WAMP) Netzwerk Protokolls zur Verfügung.

WebSocket erlaubt einen bidirektionalen, real-time Nachrichtenaustausch über das Web und WAMP fügt asynchrone Remote Procedure Calls und Publish & Subscribe Mechanismen auf der Basis von WebSockets hinzu.

WAMP ist ideal für verteilte, Client/Server Anwendungen, z.B. Mehrbenutzer- und Datenbank-gestützte Business-Anwendungen, Sensor-Netzwerk (IoT), Instant Messaging oder MMOGs (massively multi-player online games).

Autobahn|Python

Autobahn|Python benutzt dabei entweder Twisted oder Asyncio als darunterliegende Module

Twisted

Twisted ist eine Event-basierte Bibliothek für Low-Level Netzwerk-Anwendungen. Derzeit laufen noch nicht alle Teile auch unter Python 3.

Asyncio

Asyncio ist ein Standard-Modul unter Python 3 welches eine Infrastruktur für die Programmierung von single-threaded concurrent Code mittels Coroutines, mittels gemultiplextem I/O Zugriff über Sockets und anderen Ressourcen, bzw. von lauffähigen Netzwerk-Client/Server Anwendungen und anderen ähnlichen Primitiven.

Crossbar.io

Crossbar.io ist ein Open-Source Router für das offene WAMP Protokoll. Zusammen mit den Open Source WAMP Client Bibliotheken ist Crossbar.io eine Kopplungsstruktur für Komponenten in verteilten Anwendungen.

Übersicht Python WebSockets