вторник, 25 июня 2013 г.

Отлов запутанных змей.






Есть много разных способов отлова змей. Например, можно ловить руками, держа за голову и туловище. А потом аккуратно класть змею в мешок. Или можно использовать "змеелов". Иногда используют крючок. Об этом подробнее можно почитать тут.
Мы рассмотрим другой способ. =)

Далее, будет небольшой рассказ о написании приложения на 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

Это все.
Надеюсь было понятно.
Корректировка неточностей в вышеизложенном повествовании -- приветствуется!



4 комментария:

  1. >> Это объект, который поддерживает интерфейсы, наподобие IService, IServiceCollection, IProcess и т.д. Application вернет объект, реализованный через один из вышеперечисленных интерфейсов.

    Application же все эти интерфейсы одновременно имплементит. Разве нет?

    ОтветитьУдалить
    Ответы
    1. Не. Класс Application ничего не реализовывает, он просто участвует в создании иерархии и регистрации новых интерфейсов. Регистрация происходит с помощью Componentized.
      "Application вернет объект, реализованный через один из вышеперечисленных интерфейсов." - не верная фраза. Сейчас уберу.

      Удалить
  2. >> Что значит интерфейс в Python? Это класс, который имеет функции, которые требуют реализации

    Нет) Класс с функциями, требующими реализации, - это абстрактный класс. Интерфейс - это контракт, который должен выполняться всеми предоставляющими его классами.

    ОтветитьУдалить
    Ответы
    1. Абстрактный класс в терминах С++ -- класс, который имеет хотя бы одну чисто-виртуальную функцию (т.е. вирт. функцию без реализации). Понятия интерфейса в С++ нет. Для меня разница между абстрактным классом и интерфейсом следующая: интерфейс предоставляет все методы без реализации, в то время как абстрактный класс только некоторые. Для каждого языка эти термины отличаются. Для Python я особо не нашла отличий, кроме как отличие в написании. Примеры тут: http://habrahabr.ru/post/72757/
      А для Java тоже свои определения этих понятий =)

      Удалить