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