twisted pygame

Einführung in Twisted

Hinweis

Dieser Artikel wird zur Zeit bearbeitet bzw. noch ergänzt

Theorie

Die drei Hauptkomponenten eines Twistedprogramms

Da Twisted sehr umfangreich ist, beschränke ich mich hier zunächst auf die grundlegende Struktur einer einfachen, offenen TCP Server-Client-Verbindung (ohne Autorisierung). Diese besteht aus folgenden 3 Hauptkomponenten:

Protocol

Von twisted.internet.protocol.Protocol abgeleitete Klassen repräsentieren jeweils ein Verbindungsende. Das bedeutet, für jede Verbindung wird ein Protocolobjekt erzeugt und gespeichert. Auf der Clientseite wird es zunächst wenig Sinn machen, sich zu mehreren Servern gleichzeitig zu verbinden, so dass wir hier zunächst nur ein Protocolobjekt haben werden. Auf der Serverseite sieht das schon anders aus, hier werden wir meistens mehrere Protocolobjekte zu verwalten haben, es sei denn, wir schreiben einen Server, der dem Client direkt bestimmte Möglichkeiten bietet (Lösung bestimmter Rechnungen oder Aufgaben). Einerseits wäre dies allerdings vielleicht der falsche Ansatz (und XML-RPC o.ä. eventuell ein besserer), so etwas zu implementieren, andererseits müsste man dennoch mit mehreren Protocolobjekten rechnen, es sei denn, man akzeptiert immer nur eine Verbindung.

Factory

twisted.internet.protocol.Factory erzeugt und verwaltet alle Protocolobjekte. Standardmäßig wird bei einer neuen Verbindung (Server oder Client ist egal) Factory.buildProtocol aufgerufen. Diese Methode erzeugt ein Protocolobjekt, speichert eine Referenz auf sich selbst im .factory-Attribut des gerade erzeugten Protocolobjekts und gibt dieses zurück. Dabei wird vorrausgesetzt, dass die Protocolklasse im Klassenattribut Factory.protocol gespeichert ist. Darüber hinaus gibt es einige weitere Methoden (Callbacks), die bei bestimmten Ereignissen aufgerufen werden und überschrieben werden können (beispielsweise um eine Verbindung sauber zu trennen, eine Datenbankverbindung zu schließen, ...).

Reactor

twisted.internet.reactor ist für die eigentliche Verbindung zuständig. Über reactor.run() startet man die Eventschleife. Vorher sollte man jedoch entweder einen Server oder einen Client starten, sodass auch Events (aus dem SocketLayer) ankommen. Dies geht mit reactor.listenTCP(port, factory) bzw. reactor.connectTCP(ip, port, factory). Wie man sieht, muss in beiden Fällen ein Factoryobjekt übergeben werden. Soll sich nun ein Client mit einem Server verbinden, rufen die Reactoren (auf beiden Seiten) die buildProtocol-Methode iher Factory auf erhalten das Protocol für diese Verbindung und stellen sie zur Verfügung.

Zudem gibt es mit reactor.callLater(sec, func, *args, *kwargs) die Möglichkeit, über die Eventschleife eigene Funktionen verspätet aufzurufen (time.sleep würde das gesamte Programm anhalten). Dies wird vor allem später wichtig, wenn wir Twisted mit pygame kombinieren wollen.

Ein Wort zur Praxis

Auch wenn alle Daten über den Reactor laufen, spielt dieser in der Praxis zunächst keine große Rolle. Außer beim Starten des Servers oder Clients und der Eventschleife, sowie beim verzögerten Methodenaufruf über diese, benötigt man ihn nicht, sondern lässt ihn einfach im Hintergrund laufen. Viel wichtiger sind Factory und Protocol. In Protocol wird steckt die eigentliche Logik zur Kommunikation, hier werden die eingehenden Daten (jew. einer Verbindung) ausgewertet und von hier können dann genauso Daten zurückgesandt werden. Über Factory können die einzelnen Clients (dh. Protocolobjekte) miteinander kommunizieren.

Es ist logisch, dass man Protocol nicht als Klasse nutzen kann, sondern nur als Vaterklasse, von der man seine eigene Protocolklasse ableitet. Des weiteren wird man bei den meisten Programmen auch nicht die Factory-Klasse nutzen (was jedoch ginge), sondern auch abgeleitete Klassen. Twisted stellt bereits einige von Protocol und Factory abgeleitete Klassen zur Verfügung, die man entweder direkt nutzen kann, meist jedoch auch nur als Ausgangspunkt für eigene Klassen. Dabei kann man zwischen Klassen für Standardprotokolle (HTTP, FTP, POP, IMAP, SMTP, ...) und solchen, die einen beim Schreiben eigener Protokolle helfen, unterscheiden. Ich werde hier nur auf letztere eingehen.

Praxis

Beispiel: Ein Chat

  1
  2
  3
  4
  5
  6
  7
  8
  9
 10
 11
 12
 13
 14
 15
 16
 17
 18
 19
 20
 21
 22
 23
 24
 25
 26
 27
 28
 29
 30
 31
 32
 33
 34
 35
 36
 37
 38
 39
 40
 41
 42
 43
 44
 45
 46
 47
 48
 49
 50
 51
 52
 53
 54
 55
 56
 57
 58
 59
 60
 61
 62
 63
 64
 65
 66
 67
 68
 69
 70
 71
 72
 73
 74
 75
 76
 77
 78
 79
 80
 81
 82
 83
 84
 85
 86
 87
 88
 89
 90
 91
 92
 93
 94
 95
 96
 97
 98
 99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
229
230
231
232
233
234
235
236
237
238
239
240
241
242
243
244
245
246
247
248
249
250
251
252
253
254
255
256
257
258
259
260
261
262
263
264
265
266
267
268
269
270
271
272
273
274
275
276
277
278
279
280
281
282
283
284
285
286
287
288
289
290
291
292
293
294
295
296
297
298
299
300
301
302
303
304
305
306
307
308
309
310
311
312
313
314
315
316
317
318
319
320
321
322
323
324
325
326
327
328
329
330
331
332
333
334
335
336
337
338
339
340
341
342
343
344
345
346
347
348
349
350
351
352
353
354
355
356
357
358
359
360
361
362
363
364
365
366
367
368
369
370
371
372
373
374
375
376
377
378
import sys

import Tkinter as tk
import tkMessageBox

from twisted.internet import reactor
from twisted.internet.protocol import Protocol, ClientFactory, Factory
from twisted.protocols.basic import LineReceiver
from twisted.internet import tksupport

class ChatClientProtocol(LineReceiver):
    """
    Simple Echo Client Protocol

    registers at self.factory and echos all
    incoming data to factory to log (display in the textbox)
    """

    def connectionMade(self):
        """
        Called when connection is made and self.factory is set

        registers self at factory as the protocolobject
        """
        self.factory.protocolobj = self


    def lineReceived(self, line):
        """
        Called when one line (i.e. all data until self.delimiter) was received

        simply send it to self.factory.log
        """
        self.factory.log(line)

    def connectionLost(self, reason):
        """
        Called when the connection was lost

        stop the reactor and with it the mainloop and quit the program
        """
        reactor.stop()


class ChatClientFactory(ClientFactory):
    """
    Client Factory

    holds the main program (incl, GUI) starts a server or connects
    to a client and builds the local Protocol object for that connection.
    """
    protocol = ChatClientProtocol
    protocolobj = None

    def __init__(self):
        """
        Build up the GUI
        """
        self.root = tk.Tk()
        self.root.protocol("WM_DELETE_WINDOW", self.on_quit)
        self.root.title("chatwisted")
        self.bar = tk.Frame()
        self.bar.pack(fill=tk.X, side=tk.TOP)
        self.iplab = tk.Label(self.bar, text="IP:")
        self.iplab.pack(side="left")
        self.ipent = tk.Entry(self.bar, width=15)
        self.ipent.pack(side="left")
        self.ipent.insert(tk.END, "localhost")
        self.portlab = tk.Label(self.bar, text="Port:")
        self.portlab.pack(side="left")
        self.portent = tk.Entry(self.bar, width=10)
        self.portent.pack(side="left")
        self.portent.insert(tk.END, "5555")
        self.hostbut = tk.Button(self.bar, text="host", command=self.start_server)
        self.hostbut.pack(side="left")
        self.conbut = tk.Button(self.bar, text="connect", command=self.start_client)
        self.conbut.pack(side="left")
        self.about = tk.Button(self.bar, text="about", command=self.about)
        self.about.pack(side="right")
        self.bellvar = tk.IntVar()
        self.bellcheck = tk.Checkbutton(self.bar, text="bell", variable=self.bellvar)
        self.bellcheck.pack(side="right")
        self.scrollbar = tk.Scrollbar()
        self.scrollbar.pack(side="right", fill=tk.Y)
        self.textbox = tk.Text(yscrollcommand=self.scrollbar.set)
        self.textbox.pack(fill=tk.BOTH, expand=1)
        self.textbox.config(state=tk.DISABLED)
        self.scrollbar.config(command=self.textbox.yview)
        self.bottomframe = tk.Frame()
        self.bottomframe.pack(fill=tk.X, side="bottom")
        self.entry = tk.Entry(self.bottomframe)
        self.entry.pack(side="left", fill=tk.BOTH, expand=1)
        self.entry.bind("<KeyPress-Return>", self.sendMessage)
        self.b1 = tk.Button(self.bottomframe, text="Send", command=self.sendMessage)
        self.b1.pack(side="right")

    def on_quit(self):
        """
        Called when a user tries to close the window

        ask for confirmation
        """
        if tkMessageBox.askokcancel("Quit", "Do you really wish to quit?"):
            reactor.stop()

    def clientConnectionFailed(self, connector, reason):
        reactor.stop()

    def clientConnectionLost(self, connector, reason):
        reactor.stop()

    def sendMessage(self, button=None):
        """
        Send the text from the text entry to the server

        :param button:
          dummy parameter for bind-callback
        :type button:
          tk.Button or None
        """
        line = self.entry.get()
        if self.protocolobj and line:
            self.protocolobj.sendLine(line.encode('utf8'))
            self.entry.delete(0, "end")
            self.bell()

    def log(self, line):
        """
        Appends a line (without linebreak) to the textbox (and adds a lb)
        """
        self.textbox.config(state=tk.NORMAL)
        self.textbox.insert(tk.END, line+"\n")
        self.textbox.see(tk.END)
        self.textbox.config(state=tk.DISABLED)
        self.bell()

    def bell(self):
        """
        Ring the bell if wished

        just a joke ;)
        """
        if self.bellvar.get():
            self.root.bell()

    def about(self):
        """
        Pop up an info message
        """
        text = """
            Chatwisted
simple chat with Tkinter and Twisted

(cc) by Julian Habrock, 31.1.09
some rights reserved (cc by-nc-sa)
more infos on bytemuehle.de
"""
        tkMessageBox.showinfo("About", text)

    def start_client(self, ip=None, port=None):
        """
        Start a client and connect to a server
        :Parameters:
          ip : string or None
            ip to connect to. If None, the value of self.ipent is used
          port : int or None
            port to use. If None, the value of self.portent is used
        """
        port = port or self.portent.get()
        ip = ip or self.ipent.get()
        if ip and port:
            reactor.connectTCP(ip, port, self)

    def start_server(self, port=None):
        """
        Start a server and connect to it as client
        :Parameters:
          port : int or None
            port to use. If None, the value of self.portent is used
        """
        try:
            port = port or int(self.portent.get())
        except ValueError:
            tkMessageBox.showerror("Wrong Port", "Port must be a number")

        if port:
            reactor.listenTCP(port, ChatServerFactory())
            self.start_client("localhost", port)


class ChatServerProtocol(LineReceiver):
    """
    Server Protocol

    Represents a client on the server, holds its data (name) and gives the
    user interface (ie. encodes the commands)
    """
    prompt = "Whats your name?"
    welcome = "<Use /command to execute commands and /help to get a list of all commands.>"
    helptext = """
<Currently, the following commands are supported:                        >
</list           - get a list of all connected people                    >
</rename newName - rename yourself                                       >
</me action      - do sth. (this will produce a "<username action>" line >
</help           - print this help message                               >
"""
    whatdoyoumean = "<Hu? What do you mean? Try /help>"

    def connectionMade(self):
        """
        Called when connected

        ask the new client for his name by sending the prompt
        """
        self.name = None
        self.sendLine(self.prompt)

    def connectionLost(self, reason):
        """
        Called when disconnected

        logout at factory
        """
        self.factory.logout(self, reason)

    def lineReceived(self, line):
        """
        Called when one line (i.e. all data until self.delimiter) was received

        if line is the first line, register at factory with line as name.
        if not, parse line for commands and interpret it (or simply broadcast it)
        """
        line = line.rstrip()
        if not self.name:
            self.name = self.factory.register(self, line)
            self.sendLine(self.welcome)
        elif line.startswith("/"):
            if " " in line:
                cmd, arg = line[1:].split(" ", 1)
            else:
                cmd, arg = line[1:], None
            if cmd in ("nick", "rename"):
              if not arg:
                  self.sendLine("<Your nick is %s, to change pass a new name!>" % self.name)
              elif arg == self.name:
                  self.sendLine("<Your nick is allready %s!>" % self.name)
              else:
                  self.name = self.factory.rename(self, arg)
            elif cmd in ("list", "clients", "all"):
              self.factory.get_list(self)
            elif cmd == "me" and arg:
              self.factory.do(self, arg)
            elif cmd == "help":
              self.sendLine(self.helptext)
            else:
              self.sendLine(self.whatdoyoumean)
        else:
          self.factory.say(self, line)



class ChatServerFactory(Factory):
    """
    Server Factory

    Creates a new Protocol object for every connecting client and
    manages all clients and their interaction
    """
    protocol = ChatServerProtocol
    clients = {}
    client_no = 0

    def register(self, client, name):
        """
        Called from a client (ChatServerProtocol) to register itself as new client

        searches for a valid name and add `client` to the client dict

        :Parameters:
          client : ChatServerProtocol
            reference to the client that wants to register
          name : string
            preferred name of the new client
        :return:
            name of the client (may be different to `name` when there is
            allready a client with that name
        :rtype: string
        """
        if name in self.clients:
            i = 2
            while name+str(i) in self.clients:
              i+= 1
            name += str(i)
        self.clients[name] = client
        self.client_no += 1
        client.sendLine("{registered as %s}" % name)
        self.broadcast("<All welcome %s, client no. %i!>" % (name, self.client_no))
        return name

    def logout(self, client, reason):
        """
        Called from a client (ChatServerProtocol) to log out

        removes `client` from the client dict

        :Parameters:
          client : ChatServerProtocol
            reference to the client that wants to log out
          reason : string
            currently not used
        """
        del self.clients[client.name]
        self.broadcast("<%s has gone away>" % client.name)

    def rename(self, client, new_name):
        """
        Called from a client (ChatServerProtocol) to rename itself

        validates `new_name` and returns the new name

        :Parameters:
          client : ChatServerProtocol
            reference to the client that wants to get a new name
          new_name : string
            preferred new name
        :return:
            name of the client (may be different to `name` when there is
            allready a client with that name
        :rtype: string
        """
        if new_name in self.clients:
            i = 2
            while new_name+str(i) in self.clients:
              i+= 1
            new_name += str(i)
        self.clients[new_name] = client
        del self.clients[client.name]
        self.broadcast("<%s renamed to %s>" % (client.name, new_name))
        return new_name

    def get_list(self, client):
        """
        sends a priv message to `client` with a list of all connected clients
        """
        clients = " ".join(filter(lambda n: n != client.name, self.clients.keys()))
        if len(self.clients) == 1:
            client.sendLine("<You are alone on this server. Totally lost. Be nice!>")
        else: client.sendLine("<%i people are currently connected: %s and you>" %
                                                      (len(self.clients), clients))


    def say(self, speaker, line):
        """
        broadcasts a message to all clients that `speaker` says `line`
        """
        line = "<%s>: %s" % (speaker.name, line)
        self.broadcast(line)

    def do(self, actor, action):
        """
        broadcasts a message to all clients that `speaker` does `line`
        """
        line = "<%s %s>" % (actor.name, action)
        self.broadcast(line)

    def broadcast(self, line):
        """
        sends `line` to all clients
        """
        for client in self.clients.itervalues():
            client.sendLine(line)



if __name__ == "__main__":
    factory = ChatClientFactory()
    tksupport.install(factory.root)
    reactor.run()
tags: Netzwerkprogrammierung , Programmieren , PyGame , Python & Twisted erstellt am 30.1.2009 20:38, zuletzt gendert am 1.2.2009 0:06