Есть много разных способов отлова змей. Например, можно ловить руками, держа за голову и туловище. А потом аккуратно класть змею в мешок. Или можно использовать "змеелов". Иногда используют крючок. Об этом подробнее можно почитать тут.
Мы рассмотрим другой способ. =)
Далее, будет небольшой рассказ о написании приложения на Twisted.
Рассказ представляет собой частичную копипасту отсюда.
Для запуска приложений, написанных с помощью Twisted'а, можно использовать и обычный .py, но рекомендуется использовать утилиту twistd. Она считается более универсальной, кроссплатформенной, помогает настроить демонизацию с логгированием, выбрать несколько приложений, реактор и еще много всего.
Центральный компонент при запуске твистед-приложения -- класс twisted.application.service.Application. Это класс, который поддерживает интерфейсы наподобие IService, IServiceCollection, IProcess и т.д. Application должен по-умолчанию быть родителем одного из вышеперечисленных интерфейсов. Подробнее об Application, чуть ниже.
Что это за интерфейсы? Что значит интерфейс в Python?
Это класс, который имеет функции, которые требуют реализации. На примере будет понятнее, как это организовано в Twisted:
IService.
from zope.interface import implements, Interface, Attribute
class IService(Interface):
"""
A service.
Run start-up and shut-down code at the appropriate times.
@type name: C{string}
@ivar name: The name of the service (or None)
@type running: C{boolean}
@ivar running: Whether the service is running.
"""
def setName(name):
"""
Set the name of the service.
@type name: C{str}
@raise RuntimeError: Raised if the service already has a parent.
"""
def setServiceParent(parent):
"""
Set the parent of the service. This method is responsible for setting
the C{parent} attribute on this service (the child service).
@type parent: L{IServiceCollection}
@raise RuntimeError: Raised if the service already has a parent
or if the service has a name and the parent already has a child
by that name.
"""
def disownServiceParent():
"""
Use this API to remove an L{IService} from an L{IServiceCollection}.
This method is used symmetrically with L{setServiceParent} in that it
sets the C{parent} attribute on the child.
@rtype: L{Deferred<defer.Deferred>}
@return: a L{Deferred<defer.Deferred>} which is triggered when the
service has finished shutting down. If shutting down is immediate,
a value can be returned (usually, C{None}).
"""
def startService():
"""
Start the service.
"""
def stopService():
"""
Stop the service.
@rtype: L{Deferred<defer.Deferred>}
@return: a L{Deferred<defer.Deferred>} which is triggered when the
service has finished shutting down. If shutting down is immediate,
a value can be returned (usually, C{None}).
"""
def privilegedStartService():
"""
Do preparation work for starting the service.
Here things which should be done before changing directory,
root or shedding privileges are done.
"""
Это интерфейс сервиса, в каждой функции которого нет реализации, а лишь описание того, что предпологается реализовать в будущем.
Внутри zope.interface можно найти много всего интересного. Библиотека zope реализуется с помощью инверсии управления (IoC).
Подробнее о реализации абcтрактных классов и интерфейсов тут.
Чтобы создать класс, который является реализацией интерфейса нужно написать, например, так:
class Service:
"""
Base class for services. ...
"""
implements(IService)
running = 0
name = None
parent = None
def __getstate__(self):
...
def setName(self, name):
...
def setServiceParent(self, parent):
...
def disownServiceParent(self):
...
def privilegedStartService(self):
...
def startService(self):
...
def stopService(self):
...
Рассмотрим IServiceCollection. Это просто коллекция сервисов (под сервисом понимается то, что требует отдельного запуска и останова).
Коллекция может содержать различные сервисы и управлять их включением и выключением. У класса, наследующего IServiceCollection, должны быть реализованы следующие методы:
-- получение имени сервиса
-- получение итератора для обхода всех сервисов
-- добавление сервиса (объекта, унаследованного от IService), этот метод должен использоваться в setServiceParent из сервиса
-- удаление сервиса через removeService, который также используется в disownServiceParent из полученного сервиса.
class IServiceCollection(Interface):
"""
Collection of services.
Contain several services, and manage their start-up/shut-down.
Services can be accessed by name if they have a name, and it
is always possible to iterate over them.
"""
def getServiceNamed(name):
...
def __iter__():
...
def addService(service):
...
def removeService(service):
...
Если необходимо, можно самим реализовать эти интерфейсы. Однако, в Twisted уже существуют классы, которые можно считать реализацией вышеприведенных интерфейсов:
twisted.application.service.Service
twisted.application.service.Process
twisted.application.service.MultiService
Для запуска из twisted сразу нескольких сервисов, обычно используется twisted.application.service.MultiService.
Пример: хотим мы запустить 2 сервиса, которые будут передавать данные поверх протоколов UDP & TCP. Что для этого понадобится?
Смотрим, какие сервисы предоставляет Twisted():
- UDPServer
- UDPClient
- TCPServer
- TCPClient
и другие.
Каждый из этих сервисов имеет соответсвущие методы запуска и прослушивания (connect/listen) в reactor'e.
Например:
- reactor.listenTCP
- reactor.connectTCP
и т.д.
Рассмотрим подробнее один из примеров:
from twisted.application import internet, service
from twisted.names import server, dns, hosts
port = 53
# Create a MultiService, and hook up a TCPServer and a UDPServer to it as
# children.
dnsService = service.MultiService()
hostsResolver = hosts.Resolver('/etc/hosts')
tcpFactory = server.DNSServerFactory([hostsResolver])
internet.TCPServer(port, tcpFactory).setServiceParent(dnsService)
udpFactory = dns.DNSDatagramProtocol(tcpFactory)
internet.UDPServer(port, udpFactory).setServiceParent(dnsService)
# Create an application as normal
application = service.Application("DNSExample")
# Connect our MultiService to the application, just like a normal service.
dnsService.setServiceParent(application)
Возникают следующие вопросы:
1) Какие параметры принимает наше приложение?
Application(name, uid=None, gid=None)
Что это за name и как оно используются? Почему именно Application становится родителем нашего сервиса? Непонятно.
На самом деле, посмотрев на исходники, вот что можно сказать:
def Application(name, uid=None, gid=None):
"""
Return a compound class.
Return an object supporting the L{IService}, L{IServiceCollection},
L{IProcess} and L{sob.IPersistable} interfaces, with the given
parameters. Always access the return value by explicit casting to
one of the interfaces.
"""
ret = components.Componentized()
for comp in (MultiService(), sob.Persistent(ret, name), Process(uid, gid)):
ret.addComponent(comp, ignoreClass=1)
IService(ret).setName(name)
return ret
components.Componentized() позволяет с помощью zope библиотеки зарегистрировать наши будущие объекты, которые мы потом когда-нибудь создадим.
components.Componentized() -- "I am a mixin to allow you to be adapted in various ways persistently." Т.е. это класс, который участвует во множественном наследовании и позволяет дополнить производные классы некоторыми общими данными. В данном случае, мы, у каждого из позже реализованных объектов, точно добавляем название name.
uid, gid нужны, если мы собираемся реализовывать собственный процесс, т.е. будем реализовывать IProcess.
В итоге получается иерархия:
Application
|- MultiService
|-|- TCPServer
|-|- UDPServer
2) Что нужно, чтобы установить TCP & UDP в качестве сервисов?
TCPServer:
internet.TCPServer(port, tcpFactory).setServiceParent(dnsService)
Данная запись означает, что internet.TCPServer принимает порт и фабрику. Порт - это просто числовое значение порта, по которому происходит соединение.
С фабрикой посложнее.
Мы можем создать либо собственную фабрику, либо воспользоваться одним из производных классов, описанном тут.
Фабрика занимается созданием протоколов, именно поэтому она необходима практически в каждом сервисе.
Тоже самое в UDPServer, но там в качестве фабрики выступает процесс создания протокола из наследников класса twisted.internet.protocol.DatagramProtocol. Все потому, что протокол UDP работает с дейтаграммами. А они передают пакеты через некоторую среду, без предварительного установления соединения или создания виртуального канала(подробнее тут).
Либо можно создать свой класс-протокол, унаследовавшись от DatagramProtocol.
Теперь осталось написать нужные нам фабрики. Соединить протоколы. Создать необходимые сервисы. После этого назревает вопрос, как же запускать и отслеживать изменения в сложной системе? Как отлаживать?
Для этого, используем логгирование. В twisted-приложениях логгирование может выглядеть следующим образом:
from twisted.application.service import Application
from twisted.python.log import ILogObserver, FileLogObserver
from twisted.python.logfile import DailyLogFile
application = Application("myapp")
logfile = DailyLogFile("my.log", "/tmp")
application.setComponent(ILogObserver, FileLogObserver(logfile).emit)
DailyLogFile -- создает лог каждый день в новом файле.
ILogObserver -- это специальный компонент логгирования, который переопределяет стандартный метод логгирования в twisted. Это и интерфейс, который помогает "сделать что-нибудь с логами". Имеет следующий вид:
class ILogObserver(Interface):
"""
An observer which can do something with log events.
Given that most log observers are actually bound methods, it's okay to not
explicitly declare provision of this interface.
"""
def __call__(eventDict):
"""
Log an event.
@type eventDict: C{dict} with C{str} keys.
@param eventDict: A dictionary with arbitrary keys. However, these
keys are often available:
- C{message}: A C{tuple} of C{str} containing messages to be
logged.
- C{system}: A C{str} which indicates the "system" which is
generating this event.
- C{isError}: A C{bool} indicating whether this event represents
an error.
- C{failure}: A L{failure.Failure} instance
- C{why}: Used as header of the traceback in case of errors.
- C{format}: A string format used in place of C{message} to
customize the event. The intent is for the observer to format
a message by doing something like C{format % eventDict}.
"""
Из этого следует, что этот интерфейс пытается выполнить действие со словарем, в котором определены такие параметры как:
- текстовый лог
- тип ошибки
- формат вывода
- какие исключения были
и т.д.
setComponent позволяет установить данное логгирование для каждого из сервисов в MultiService.
FileLogObserver позволяет произвести логгирование в некоторый файл. Метод emit работает со словарем из ILogObserver.
class FileLogObserver:
"""
Log observer that writes to a file-like object.
@type timeFormat: C{str} or C{NoneType}
@ivar timeFormat: If not C{None}, the format string passed to strftime().
"""
timeFormat = None
def __init__(self, f):
self.write = f.write
self.flush = f.flush
def emit(self, eventDict):
text = textFromEventDict(eventDict)
if text is None:
return
timeStr = self.formatTime(eventDict['time'])
fmtDict = {'system': eventDict['system'], 'text': text.replace("\n", "\n\t")}
msgStr = _safeFormat("[%(system)s] %(text)s\n", fmtDict)
util.untilConcludes(self.write, timeStr + " " + msgStr)
util.untilConcludes(self.flush) # Hoorj!
Не так важно, что именно будет использоваться в качестве объекта, в который идет лог. Главное чтоб этот объект работал со словарем eventDict и имел метод emit(испускать).
Например, можно использовать PythonLoggingObserver:
class PythonLoggingObserver(object):
"""
Output twisted messages to Python standard library L{logging} module.
WARNING: specific logging configurations (example: network) can lead to
a blocking system. Nothing is done here to prevent that, so be sure to not
use this: code within Twisted, such as twisted.web, assumes that logging
does not block.
"""
def __init__(self, loggerName="twisted"):
"""
@param loggerName: identifier used for getting logger.
@type loggerName: C{str}
"""
self.logger = logging.getLogger(loggerName)
def emit(self, eventDict):
"""
Receive a twisted log entry, format it and bridge it to python.
By default the logging level used is info; log.err produces error
level, and you can customize the level by using the C{logLevel} key::
>>> log.msg('debugging', logLevel=logging.DEBUG)
"""
if 'logLevel' in eventDict:
level = eventDict['logLevel']
elif eventDict['isError']:
level = logging.ERROR
else:
level = logging.INFO
text = textFromEventDict(eventDict)
if text is None:
return
self.logger.log(level, text)
def start(self):
"""
Start observing log events.
"""
addObserver(self.emit)
def stop(self):
"""
Stop observing log events.
"""
removeObserver(self.emit)
Для запуска рекомендуется использовать утилиту twistd с параметрами -y -n.
По умолчанию twistd запускает приложение в режиме демонизации и записывает логи в файл twistd.log. Чаще всего хочется запускать программу в приоритетном режиме(foregraund), поэтому используется ключ -n (--nodaemon). Для запуска файла с расширением .tac необходимо использовать ключ -y (с помощью этого ключа в *.tac ищется переменная application и исполнение начинается посредством запуска twisted.application.service.Application).
Итог:
twistd -n -y nameserver.tac
Это все.
Надеюсь было понятно.
Корректировка неточностей в вышеизложенном повествовании -- приветствуется!
>> Это объект, который поддерживает интерфейсы, наподобие IService, IServiceCollection, IProcess и т.д. Application вернет объект, реализованный через один из вышеперечисленных интерфейсов.
ОтветитьУдалитьApplication же все эти интерфейсы одновременно имплементит. Разве нет?
Не. Класс Application ничего не реализовывает, он просто участвует в создании иерархии и регистрации новых интерфейсов. Регистрация происходит с помощью Componentized.
Удалить"Application вернет объект, реализованный через один из вышеперечисленных интерфейсов." - не верная фраза. Сейчас уберу.
>> Что значит интерфейс в Python? Это класс, который имеет функции, которые требуют реализации
ОтветитьУдалитьНет) Класс с функциями, требующими реализации, - это абстрактный класс. Интерфейс - это контракт, который должен выполняться всеми предоставляющими его классами.
Абстрактный класс в терминах С++ -- класс, который имеет хотя бы одну чисто-виртуальную функцию (т.е. вирт. функцию без реализации). Понятия интерфейса в С++ нет. Для меня разница между абстрактным классом и интерфейсом следующая: интерфейс предоставляет все методы без реализации, в то время как абстрактный класс только некоторые. Для каждого языка эти термины отличаются. Для Python я особо не нашла отличий, кроме как отличие в написании. Примеры тут: http://habrahabr.ru/post/72757/
УдалитьА для Java тоже свои определения этих понятий =)