Twisted - wprowadzenie: implementacja protokołu

W poprzednim wpisie przedstawiłem obiekt reaktora oraz klasę Deferred. Kolejnym krokiem będzie napisanie serwera, który używając prostego protokołu będzie zwracał kod strony której adres został przesłany przez klienta.

Implementacja protokołu

Bazową klasą każdego protokołu, implementującą jedynie IProtocol jest internet.protocol.Protocol. Podstawową klasą dla wszystkich serwerów jest internet.protocol.ClientFactory:

>>> from twisted.internet import protocol, reactor
>>> 
>>> class WebGetProtocol(protocol.Protocol):
...     pass
... 
>>> class WebGetFactory(protocol.ServerFactory):
...     protocol = WebGetProtocol
...     
>>> reactor.listenTCP(9000, WebGetFactory())
<<class 'twisted.internet.tcp.Port'> of __main__.WebGetFactory on 9000>
>>> reactor.run()

Po utworzeniu instancji fabryki, przekazuję ją wraz z numerem portu jako parametr metody listenTCP obiektu reaktora. Wszelkie dane które pojawią się na tym porcie, obsłużone zostaną przez moją fabrykę.

Do połączenia z serwerem, można użyć na przykład programu telnet:

$ telnet localhost 9000
Trying 127.0.0.1...
Connected to localhost.
Escape character is '^]'.
http://google.pl
...

Klasy Protocol i Factory

Ponieważ w twisted zastosowano wzorzec fabryki, przy implementacji nowego protokołu zawsze trzeba użyć co najmniej dwóch klas. Obiekt fabryki jest właściwym serwerem (lub klientem). Zarządza połączeniami i tworzy nowe instancje klasy protokołu, które odpowiedzialne są za obsługę konkretnego połączenia.

Logowanie ruchu

Pierwszą funkcjonalnością serwera będzie monitorowanie połączeń:

from twisted.python import log
log.startLogging(open('/tmp/myserver.log', 'w'))

class WebGetProtocol(protocol.Protocol):

    def connectionMade(self):
        log.msg('new connection')

    def connectionLost(self, reason=None):
        log.msg('connection lost: %s', reason)

Aby zapisywać wszystkie nawiązane i zerwane połączenia, zaimplementowałem metody connectionMade oraz connectionLost. Do logowania, zamiast modułu logging użyłem twisted.python.log.

$ tail -f /tmp/myserver.log
...
2009-09-28 01:11:21+0200 [__main__.WebGetFactory] new connection
2009-09-28 01:11:24+0200 [WebGetProtocol,0,127.0.0.1] connection lost: %s
[Failure instance: Traceback (failure with no frames): <class
'twisted.internet.error.ConnectionDone'>: Connection was closed cleanly.
     ]

Przesyłanie danych

Kolejnym krokiem będzie czytanie i wysyłanie danych, które zakończone będą znakiem powrotu karetki i nowego wiersza. Pomysł używania \r\n jest na tyle popularny, że w twisted jest już gotowa klasa do wymiany danych w ten sposób. LineReceiver implementuje ten sam interfejs co protocol.Protocol, więc nie trzeba zmieniać napisanego do tej pory kodu.

from twisted.web.client import getPage
from twisted.protocols.basic import LineReceiver

class WebGetProtocol(LineReceiver):

    def lineReceived(self, line):
        deferr = getPage(line)
        deferr.addCallback(self.send_success).addErrback(self.send_errback)

    def send_success(self, data):
        data = data.replace('\r\n', '')
        self.sendLine(data)

    def send_errback(self, err):
        self.sendLine('Server error')
        self.transport.loseConnection()

Po otrzymaniu danych (lineReceived), pobieram stronę znajdującą się pod przesłanym adresem. Nie wiadomo jednak jak szybko odpowie serwer, dlatego zamiast czekać na zakończenie przesyłania danych używam getPage, a do zwróconego obiektu Deferred dodaję własny callback i errback . Reaktor nie jest blokowany i czekając na odpowiedź, może uruchomić inne zadania.

W przypadku wystąpienia błędu chcę zerwać połączenie z klientem. Potrzebny jest do tego obiekt typu WebGetFactory. Każdy protokół posiada jednak referencję do swojej fabryki, która trzymana jest pod atrybutem transport. Ostatnia linia metody send_errback odpowiada więc za zerwanie połączenia.

Aplikacja Twisted

Pisząc program zawsze trzeba dodać sporo powtarzającego się kodu. Demonizacja aplikacji, logowanie, uruchamianie i wyłączanie programu to tylko te najczęściej występujące funkcjonalności. Twisted zawiera na szczęście interfejs który pozwala zaimplementować to wszystko w paru linijkach.

Z dotychczas napisanego kodu usunąć można ostatnie linijki. twistd sam zajmie się wyborem odpowiedniego reaktora i jego uruchomieniem. Jedyne co trzeba zrobić to utworzyć obiekt o nazwie application:

from twisted.application import internet, service

# ...

#reactor.listenTCP(9000, WebGetFactory())
#reactor.run()

application = service.Application("webget")
webget = internet.TCPServer(9000, WebGetFactory())
webget.setServiceParent(application)

Plik zapisać należy z rozszerzeniem .tac, które informować będzie, że kod Pythona znajdujący się w tym pliku zawiera obiekt Application i uruchamiać go należy używając twistd:

$ twistd -noy simple_server.tac -l /tmp/myserver.log

Kod użyty jako przykłady pobrać można z github