Image

От Perl-скрипта к Twisted-приложению: Черновик

Originally published at Pythy. You can comment here or there.

Итак, в наличии есть работающий Perl-скрипт и некое описания протокола (некое,

потому что это логи уже работающего "концентратора" и небольшие комментарии по

тексту).


Согласно принятому дизайну Twisted-приложений, общий

функционал разбивается на модули, причем разделяется реализация собственно

протокола и "сторонней логики". В нашем случае, "сторонняя логика" - это

выборка имени человека по номеру договора.


Поскольку у товарища стоит такая СУБД, которой у меня нет, да и особо ставить

не хочется, я ввел, опять-таки общепринятый для Twisted-приложений, механизм

интерфейсов и классов, реализующих их.


Поясню: мне необходимо на основании одних данных (номера договора) получать

другие данные (фамилию, имя и отчество человека). Сделать это можно различными

способами, данные могут храниться в различных СУБД и т.д. Поэтому я создаю

класс-описание, интерфейс, который описывает какие методы и атрибуты

необходимы для реализации, но саму реализацию не указывает. Общепринято такие

классы-интерфейсы называть "IName", где Name — общее имя интерфейса. У меня

получился такой интерфейс:


from zope.interface import Interface, Attribute

class IClient(Interface):
    """An object that returns info about client"""

    encoding = Attribute("Encoding of client's backend")

    def getClient(agreem_number):
        """Returns an info about client"""


В Twisted используется механизм интерфейсов из Zope. В отличии

от "обычных" классов, в классах-интерфейсах не надо в методах ставить

первым параметром self.


Итак, теперь у нас есть описание, каким должен быть класс: у него должен быть

атрибут encoding (потому что кодировка СУБД может не совпадать с кодировкой

транспорта) и метод getClient, получающий один параметр agreem_number.


Далее, реализуем "тупой" клиент в тестовых целях.


import time

from zope.interface import implements

# полагаем, что IClient у нас либо импортирован, либо находится
# в том же модуле

class DummyClient(object):
    """Dummy client for testing purposes"""
    implements(IClient)

    def __init__(self, encoding=None):
        self.encoding = encoding
        self.pause_time = 0

    def getClient(self, agreem_num):
        res = 'Dummy_%s' % agreem_num
        # в описании было одно требование: чтобы возвращаемый результат
        # не был длиннее 20 символов
        if len(res) > 20:
            res = res[:20]
        # для имитации задержки выборки данных
        time.sleep(self.pause_time)

        return res


Теперь у нас уже есть работающий клиент, так что пора браться за реализацию

протокола. Поскольку в нашем случае протокол текстовый, т.е. данные передаются

построчно, то я за основу взял LineReceiver.


from twisted.protocols import basic
from twisted.python import log

class PythyProto(basic.LineReceiver):
    """Simple text demo protocol"""

    def connectionMade(self):
        log.msg("got connection from %s" % str(self.transport.client))

    def connectionLost(self, reason):
        log.msg("connection from %s lost" % str(self.transport.client))

    def lineReceived(self, line):
        log.msg("data received from %s: `%s'" % (str(self.transport.client), line))
        if line == '':
            # для тестовых целей
            self.sendLine("Good bye")
            self.transport.loseConnection()
            return
        agr = line[10:15]
        client = self.factory.clients.getClient(agr)
        self.sendAnswer(client)

    def sendAnswer(self, client):
        packet = "dummypacketmaker012345678%s" % client
        self.sendLine(packet)

    def sendLine(self, line):
        assert isinstance(line, basestring)
        line = line.replace('r', '').replace('n', '')
        log.msg("data send: %s" % line)
        line = line + "rn"

        self.transport.write(line)

Экземпляр класса протокол создается для каждого соединения. Т.е. некая неизменная

информация (например, кодировка данного протокола) не сохраняется, для сохранения

такого рода информации существуют "фабрики" (factory):


from twisted.internet import protocol

# полагаем, что PythyProto у нас либо импортирован, либо находится
# в том же модуле

class PythyFactory(protocol.ServerFactory):
    protocol = PythyProto   # class, not instance!

    def __init__(self, clients, transport_encoding):
        self.clients = clients
        self.encoding = transport_encoding

Теперь собираем всё "в кучу" и пробуем запустить:


import sys

from twisted.internet import reactor
from twisted.python import log

from TwistedPythy import proto, clients

log.startLogging(sys.stderr)
client = clients.DummyClient()
client.pause_time = 0
factory = proto.PythyFactory(client, 'utf-8')
reactor.listenTCP(3000, factory)
reactor.run()

Первый рабочий "черновик" создан. Пока главное, что он вообще работает :-)

Кодировки будут следующим этапом.


А для разминки советую попробовать поставить паузу в DummyClient секунд в

10-20 и попробовать одновременно сделать запрос двумя клиентами.


P.S. К сожалению, движок блога не дает загрузить архив исходников, поэтому я загрузил на RapidShare.