Fork me on GitHub
16/7/2006

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

Итак, в наличии есть работающий 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 + "\r\n"

        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.

Комментарии

Все статьи