smart | Webentwicklung
Alles rund um HTML5, PHP, WordPress & Co.

PubSub-Klasse für Ruby-EventMachine WebSocket-Server

7. Mai 2012
Stephan
Ruby

Für eine Webapplikation, bei der verschiedene (Teil-)Funktionalitäten auf einer WebSocket-Kommunikation zwischen Client und Server basieren, kann es sinnvoll sein, ein Publish-Subscribe-System einzusetzen.

Nehmen wir z.B. eine Webapplikation mit integriertem Chat, wobei die Nutzer (Clients) verschiedene Chat-Räume nutzen können. Angenommen es gibt zwei Chat-Räume mit folgenden Nutzern (Clients): Chat-Raum A (Ingo, Hanna, Tom) und Chat-Raum B (Ben, Toni). Wenn nun Ingo in Chat-Raum A eine Nachricht schreibt, dann soll die Nachricht auch nur an Hanna und Tom gesendet werden und somit nur in Chat-Raum A sichtbar sein. Ben und Toni in Chat-Raum B jedoch sollen diese Nachricht nicht erhalten. Für einen solchen Anwendungsfall ist nun der Einsatz eines Publish-Subscribe-Systems nützlich.

In diesem Artikel zeige ich euch, wie ihr eine grundlegende PubSub-Klasse in Ruby implementieren könnt und sie mittels einem auf Ruby-EventMachine basierenden WebSocket-Server nutzen könnt.

Was ist das Prinzip des Publish-Subscribe-Entwurfsmusters?

Publish-Subscribe bedeutet auf deutsch soviel wie „veröffentlichen und abbonnieren“.

In unserem Beispiel-Fall ist es so, dass es auf der Seite des WebSocket-Servers zwei Channels gibt. Einer ist für die Kommunikation von Chat-Raum A und einer für Chat-Raum B. Die Nutzer (Clients) melden sich nun beim Server und teilen mit, welchen Channel sie abonnieren bzw. welchem Channel sie beitreten wollen.

Wenn Ingo also den Channel für Chat-Raum A abbonniert hat, empfängt er alle für diesen Channel relevanten Nachrichten und seine Nachrichten werden an alle in diesem Channel registrierten Nutzer geschickt.

Für nähere Informationen zum Publish-Subscribe-Entwurfsmuster verweise ich auf den Publish-Subscribe-Pattern-Eintrag bei Wikipedia.

JSON als Nachrichten-Format

Um nun unterscheiden zu können, ob ein Nutzer einem Channel beitreten, wieder austreten oder etwas veröffentlichen möchte, benötigen wir ein Unterscheidungsmerkmal in den Nachrichten. Hierfür können wir JSON als Nachrichten-Format verwenden.

Das Grundgerüst einer Nachricht, so wie sie der Nutzer (Client) an den WebSocket-Server sendet, könnte z.B. wie folgt aussehen:

var message = {
      type: this.CHAT_TYPE_USER_MESSAGE,
      pubsub: {
            type: this.PUBSUB_TYPE_PUBLISH,
            channel: this.get('chatRoom')
      },
      data: {
            username: this.get('username'),
            text: text
      }
};

Wir haben ein einfaches Nachrichten-Objekt, dass mehrere Daten beinhaltet. Zum einen geben wir den Nachrichten-Typ (Zeile 2) an.

In diesem Beispiel könnte der Typ bedeuten, dass die Nachricht eine einfache Chat-Nachricht ist.

Weiterhin definieren wir unseren PubSub-Typ (Zeile 3 – 6) und den jeweiligen Channel. In diesem Fall würden wir die Nachricht im angegebenen Channel veröffentlichen wollen. Danach geben wir dann die eigentlichen Daten bzw. die eigentliche Nachricht an (Zeile 7 – 10).

Grundgerüst der PubSub-Klasse

Das Grundgerüst für die PubSub-Klasse könnte z.B. so aussehen:

class PubSub
    TYPE_PUBLISH = 0
    TYPE_SUBSCRIBE = 1
    TYPE_UNSUBSCRIBE = 2

    def initialize
        @clients = {}
        @channels = {}
    end

    def subscribe(channel_id, client_id, client_websocket)
    end

    def unsubscribe(channel_id, client_id)
    end

    def unsubscribe_all(client_id)
    end

    def publish(channel_id, message)
    end
end

Wie zu sehen ist, werden am Anfang (Zeile 2 – 4) drei Konstanten definiert, die die jeweiligen PubSub-Typen repräsentieren.

Danach implementieren wir einen Konstruktor und erstellen eine Hash-Liste für die Clients und für die Channels (Zeile 6 – 9). Anschließend sind die Grundgerüste für die benötigten Methoden implementiert.

Subscribe

Die subscribe-Methode dient dazu, einen Client für einen bestimmten Channel zu registrieren. Der Code sieht wie folgt aus:

def subscribe(channel_id, client_id, client_websocket)
    if !@clients.include? client_id
        @clients[client_id] = client_websocket
    end

    @channels[channel_id] ||= []

    if !@channels[channel_id].include? client_id
        @channels[channel_id] << client_id
    end
end

Der Methode wird zum einen die Channel- und Client-ID und zum anderen das zum Client dazugehörige WebSocket-Objekt übergeben.

Als erstes wird dann geprüft, ob der Client bereits im PubSub-System registriert ist (Zeile 2). Falls der Client noch nicht in der Client-Liste steht, dann fügen wir in die Client-Liste unter der gegebenen Client-ID das WebSocket-Objekt ein (Zeile 3).

Danach wird analog zur Client-Liste überprüft, ob in der Channel-Liste der angegebene Channel existiert und wenn nicht, wird ein neues Array unter der Channel-ID in der Channel-Liste eingefügt (Zeile 6).

Zum Abschluss überprüfen wir nur noch, ob der Client nicht bereits für den Channel registriert ist und wenn nicht, registrieren wir den Client für diesen Channel.

Unsubscribe

Damit sich ein Client auch wieder von einem Channel abmelden kann, brauchen wir eine unsubscribe-Methode:

def unsubscribe(channel_id, client_id)
    if @channels[channel_id].include? client_id
        @channels[channel_id].delete client_id
    end
end

Dabei werden wieder die Channel- und Client-ID übergeben. Die Methode macht nichts weiter als zu prüfen, ob der Client für den angegebenen Channel registriert ist und löscht ihn gegebenenfalls aus der Channel-Liste. Diese Methode sollte generell dann verwendet werden, wenn der Client weiterhin am Gesamtsystem angemeldet ist bzw. die Webapplikation weiterhin nutzt, aber z.B. einen Chat-Raum verlassen hat.

Dagegen sollte, wenn ein Client sich vom Gesamtsystem abmeldet, die folgende Methode verwendet werden:

def unsubscribe_all(client_id)
    if @clients.include? client_id
        @channels.each do |channel_id, client_list|
            if client_list.include? client_id
                client_list.delete client_id
            end
            @clients.delete client_id
        end
    end
end

Die Methode dient dazu, einen Client aus allen Channel, wo dieser registriert ist, abzumelden bzw. zu löschen. Hierfür iterieren wir einfach über die Channel-Liste (Zeile 3 – 8).

Publish

Die letzte Methode ist die publish-Methode, die zum Veröffentlichen von Nachrichten genutzt wird:

def publish(channel_id, message)
    if @channels[channel_id]
        @channels[channel_id].each do |client_id|
            @clients[client_id].send(message.to_json.to_s)
        end
    end
end

In diesem Fall brauchen wir keine Client-ID, sondern nur die Channel-ID und die Nachricht, die dann an alle im Channel angemeldeten Clients geschickt wird.

WebSocket-Server anpassen

Jetzt müssen wir nur noch unseren WebSocket-Server anpassen.

Dazu müssen wir als erstes ein neues PubSub-Objekt instanziieren:

@pub_sub = PubSub.new

# EventMachine-Loop starten
EM.run do
    [...]
end

Die onopen-Methode sieht jetzt so aus:

client_websocket.onopen do
    client_id = client_websocket.object_id
    puts 'Client ' + client_id.to_s + ' connected'
end

Anschließend passen wir unsere onmessage-Methode an:

client_websocket.onmessage do |message|
    client_id = client_websocket.object_id
    puts 'From Client ' + client_id.to_s + ' received message: ' + message

    message = JSON.parse(message)

    if message['pubsub']['type'] === PubSub::TYPE_PUBLISH
        @pub_sub.publish(message['pubsub']['channel'], message)
    elsif message['pubsub']['type'] === PubSub::TYPE_SUBSCRIBE
        @pub_sub.subscribe(message['pubsub']['channel'], client_id, client_websocket)
    elsif message['pubsub']['type'] === PubSub::TYPE_UNSUBSCRIBE
        @pub_sub.unsubscribe(message['pubsub']['channel'], client_id)
    end
end

Zum Schluss wird noch die onclose-Methode ein wenig angepasst:

client_websocket.onclose do
    client_id = client_websocket.object_id
    puts 'Client ' + client_id.to_s + ' disconnected'

    @pub_sub.unsubscribe_all(client_id)
end

Fazit

Dieser Artikel zeigt, wie ihr ein kleines Publish-Subscribe-System umsetzen könnt. Die vogestellte PubSub-Klasse ist jedoch nur ein Beispiel und bei größeren Projekten sind eventuelle auch etablierte Messaging-Systeme, wie z.B. RabbitMQ geeigneter.

Kennt ihr euch mit Messaging-Systemen aus und habt eventuell schon selbst welche in euren Projekten eingesetzt?

Kommentare  
0 Kommentare vorhanden
0 Trackbacks/Pingbacks vorhanden
Du bist herzlich eingeladen auch ein Kommentar zu hinterlassen!
Kommentar schreiben

Vielen Dank für dein Kommentar!