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

О ресторане и его клиентах.


В ресторане "Грабли" очень вкусные завтраки. Очень рекомендую.
Есть еще кровавые блинчики:



Обслуживание в Граблях намного лучше, чем в том же МУ-МУ (в МУ-МУ на Таганской особенно приставучие продавцы!)
Когда люди приходят в ресторан, они хотят хорошо поесть. Ресторан -- тот же сервер по обсуживанию клиентов. Ресторан -- сервер, предоставляющий еду. И если он хорошо выполняет свои обязанности, то и коннект клиентов будет утойчивый и скорее всего, переподключаемый.

Подробнее о клиентах с серверами под катом. А если стало интересно про ресторан "Грабли", то вам сюда.




Пример сервера с клиентом на Twisted.
Рассмотрим пример написания сервера отсюда (немного измененный).

При подключении нового клиента, сервер посылает броадкастом сообщения
"Welcome to my chat server", "Send '/NAME [new name]' to change your name." и т.д. Сервер создает соединение, добавляя клиентов и посылая всем существующим клиентам бродкаст о новом подключении. При потери соединения с клиентом, мы его удаляем и посылаем всем бродкаст о выходе клиента из чат-сервера. Если имя клиент не хочет указывать, то в качестве имени задается его хост (например: 192.168.1.1). Клиент может посылать различные сообщения серверу. Сервер, полученные сообщения, передаст всем сущетсвующим клиентам. Сервер всегда делает бродкаст и работает в режиме "эхо".

from twisted.internet import reactor
from twisted.internet.protocol import ServerFactory 
from twisted.protocols.basic import LineOnlyReceiver 

class ChatProtocol(LineOnlyReceiver): 

    name = "" 
    delimiter = '\n'
    
    def getName(self): 
        if self.name!="": 
            return self.name 
        return self.transport.getPeer().host 

    def connectionMade(self): 
        print "New connection from "+self.getName() 
        self.sendLine("Welcome to my chat server.") 
        self.sendLine("Send '/NAME [new name]' to change your name.") 
        self.sendLine("Send '/EXIT' to quit.") 
        self.factory.broadcast_send(self.getName()+" has joined the party.") 
        self.factory.clientProtocols.append(self)

    def connectionLost(self, reason): 
        print "Lost connection from "+self.getName() 
        self.factory.clientProtocols.remove(self) 
        self.factory.broadcast_send(self.getName()+" has disconnected.") 

    def lineReceived(self, line): 
        print self.getName()+" said "+line 
        if line[:5]=="/NAME": 
            oldName = self.getName() 
            self.name = line[5:].strip() 
            self.factory.broadcast_send(oldName+" changed name 
to "+self.getName()) 
        elif line=="/EXIT": 
            self.transport.loseConnection() 
        else: 
            self.factory.broadcast_send(self.getName()+" says "+line) 

    def sendLine(self, line): 
        self.transport.write(line+self.delimiter) 

class ChatProtocolFactory(ServerFactory): 

    protocol = ChatProtocol 

    def __init__(self): 
        self.clientProtocols = [] 

    def broadcast_send(self, mesg): 
        for client in self.clientProtocols:
            client.sendLine(mesg) 

print "Starting Server"
factory = ChatProtocolFactory()
reactor.listenTCP(12345, factory)
reactor.run() 

Возникает множество вопросов даже в самом простом примере? Почему так и никак иначе? Откуда неведомые переменные transport, protocol, listenTCP? и т.д.?

Попробуем разобраться.
1) В чем идея? (подробнее о серверах в twsited - тут)
У нас есть один сервер и множество клиентов. Сервер предназначен, чтобы к нему подключалось множество клиентов (connectionMade) и отправляли данные на сервер(sendLine). В данном случае, мы создали сервер. В качестве клиента можно использовать утилиту nc:
nc 192.168.1.1 12345
nc localhost 12345
...
Заметим, что nc работает со строками и в качестве конца строки использует перевод строки (\n), в то время как тот же telnet использует в качестве разделителя строк \r\n. Если посмотреть стандартную реализацию протокола LineOnlyReceiver  увидим, что там есть переменная  delimiter = '\r\n'. Ее всегда можно переопределить в своем протоколе.

2) О протоколах. Зачем они?
Для сервера и клиента можно использовать некоторый протокол, по которому будут приходить сообщения. Сообщения, в данном контексте, - любой фрагмент данных, логически объединённых для передачи (кадр, пакет, датаграмма). Допустим, в качестве сообщений у нас выступает просто строка. Для строк в Twisted как раз и используем специальный протокол: LineOnlyReceiver. Как видим, он является базовым для известных почтовых протоколов: POP3 и SMTP. Возникает вопрос: "Можем ли мы для клиента и сервера использовать одинаковый протокол?" Да. Можем. Но не факт, что этот протокол подходит для всех действий, которые мы ожидаем от клиента и от сервера.
Рассмотрим пример, с почтовыми протоколами. Есть SMTP-протокол.

"SMTP-сессия состоит из команд, посылаемых SMTP-клиентом, и соответствующих ответов SMTP-сервера."(с) wiki.

Однако,  для получения готового сообщения, такого, каким мы привыкли его видеть в той же электронной почте, используется совершенно другой протокол.

"SMTP — всего лишь протокол доставки. Он не может по требованию взять сообщения с удаленного сервера. Для извлечения почты и управления почтовым ящиком разработаны другие протоколы, такие как POP и IMAP"(c) wiki.

В Twisted  можно использовать как высокоуровневые, так и низкоуровневые протоколы. Из высокоуровневого существует IRC, который я пока еще не использовала. К низкоуровневым относят все протоколы, которые мы реализовываем сами и наследуем от protocol.Protocol.

3) Как происходит добавление клиентов в сервер? Почему через фабрику? Зачем нужны фабрики?
В twisted и без них можно обойтись, но будет не так красиво. И при необходимости, сложнее будет настроить автоматичсекое переподсоединение в случае реализации фабрики для клиента (в данном случае, фабрика используется для коннекта сервера с клиентами).
При помощи фабрики очень просто можно взять и изменить соединение, по которому идет коннект.
Не используя фабрику, придется использовать:
 - либо Deffered c Callbacks\Errbacks
 - либо Endpoints

4) Зачем нам reactor.listenTCP?
Мы захотели подключаться и передавать сообщения по TCP-соединению. Для каждого сервиса (для клиента или для сервера) есть соответсующие методы в reactor'e на соединение и на прослушивание. "connect" используется для клиентов, "listen" используется для сервера. И для нашего TCP есть соответсвующие методы:  reactor.listenTCP и reactor.connectTCP.

В данном случае, мы слушаем, пришел ли клиент на порту 12345 с фабрикой протоколов ChatProtocolFactory. Каждое соединение по отдельности осуществляется по реализованному протоколу ChatProtocol.

5) Траспорт. Зачем? Что это?
Транспорт в Twisted реализуется в соответствии с интерфейсом ITransport:

class ITransport(Interface):
    """
    I am a transport for bytes.

    I represent (and wrap) the physical connection and synchronicity
    of the framework which is talking to the network.  I make no
    representations about whether calls to me will happen immediately
    or require returning to a control loop, or whether they will happen
    in the same or another thread.  Consider methods of this class
    (aside from getPeer) to be 'thrown over the wall', to happen at some
    indeterminate time.
    """

    def write(data):
        """
        Write some data to the physical connection, in sequence, in a
        non-blocking fashion.

        If possible, make sure that it is all written.  No data will
        ever be lost, although (obviously) the connection may be closed
        before it all gets through.
        """

    def writeSequence(data):
        """
        Write a list of strings to the physical connection.

        If possible, make sure that all of the data is written to
        the socket at once, without first copying it all into a
        single string.
        """

    def loseConnection():
        """
        Close my connection, after writing all pending data.

        Note that if there is a registered producer on a transport it
        will not be closed until the producer has been unregistered.
        """

    def getPeer():
        """
        Get the remote address of this connection.

        Treat this method with caution.  It is the unfortunate result of the
        CGI and Jabber standards, but should not be considered reliable for
        the usual host of reasons; port forwarding, proxying, firewalls, IP
        masquerading, etc.

        @return: An L{IAddress} provider.
        """

    def getHost():
        """
        Similar to getPeer, but returns an address describing this side of the
        connection.

        @return: An L{IAddress} provider.
        """

Из комментариев видно, что транспорт предназначен для передачи байтов (в самом общем случае). Транспорт должен иметь метод write, т.к. именно с помощью него определяется, куда будет выведен результат.
В нашем случае будет использоваться интерфейс ITCPTransport.
Так очень быстро можно дойти и до темы с файловыми дескрипторами, которые могут быть реализацией траспортного интерфейса. Так например здесь, вы видим, что реализация начинается с определения файлового дескриптора и установления сокета.
Но пока не будем затрагивать эту тему.
Еще есть одно хорошее применение транпсорта: мы можем взять и поменять соединение на защифрованное. О том, как это сделать, подробнее тут.

Теперь немного о клиентах.
Помимо nc-клиента (nc localhost 12345), можно еще написать отдельного клиента с помощью Twisted.
Рассмотрим подробнее следующий пример:

from twisted.internet.protocol import Protocol, ReconnectingClientFactory
from sys import stdout

class Echo(Protocol):
    def dataReceived(self, data):
        stdout.write(data)

class EchoClientFactory(ReconnectingClientFactory):
    def startedConnecting(self, connector):
        print 'Started to connect.'

    def buildProtocol(self, addr):
        print 'Connected.'
        print 'Resetting reconnection delay'
        self.resetDelay()
        return Echo()

    def clientConnectionLost(self, connector, reason):
        print 'Lost connection.  Reason:', reason
        ReconnectingClientFactory.clientConnectionLost(self, connector, reason)

    def clientConnectionFailed(self, connector, reason):
        print 'Connection failed. Reason:', reason
        ReconnectingClientFactory.clientConnectionFailed(self, connector, reason)

from twisted.internet import reactor
reactor.connectTCP(host, port, EchoClientFactory())
reactor.run()

Клиент может подключаться по такому же протоколу, как у сервера, но это совершенно необязательно. Нужно помнить, что при реализации может быть целая иерархия из протоколов.
В данном случае, создали простейший протокол Echo.
Как создаем фабрику? Заметим, что у клиента в качестве базовой фабрики используется ReconnectingClientFactory, которая является подклассом ClientFactory. В то время как у сервера использовался в качеcтве базового ServerFactory.
Почему именно ReconnectingClientFactory, а не ClientFactory?
Все потому, что если мы потеряем соединение с сервером, у нас клиент отсоединиться и все. Дальнейшее соединение нужно будет проводить вручную, запустив скрипт. В то время как, при помощи этого ReconnectingClientFactory мы можем следить за сервером и как только сервер вновь заработает, клиент подключится к нему автоматически.

reactor.connectTCP как было сказано ранее, используется для подсоединения к серверу. В качестве параметров нужно передать хост, порт, по которому слушает сервер, и фабрику.

Для того, чтобы передать какое-нибудь сообщение, можем использовать reactor.callLater. Например, изменив пример клиента так:

from twisted.internet.protocol import Protocol, ReconnectingClientFactory
from sys import stdout

class Echo(Protocol):
    def dataReceived(self, data):
        stdout.write(data)

    def connectionMade(self):
        self.transport.write("Hello server, I am the client!\n")
        self.factory.server = self

    def sendMsg(self, m):
        self.transport.write(m+'\n')


class EchoClientFactory(ReconnectingClientFactory):
    protocol = Echo
    server = None

    def startedConnecting(self, connector):
        print 'Started to connect.'

    def clientConnectionLost(self, connector, reason):
        print 'Lost connection.  Reason:', reason
        ReconnectingClientFactory.clientConnectionLost(self, connector, reason)

    def clientConnectionFailed(self, connector, reason):
        print 'Connection failed. Reason:', reason
        ReconnectingClientFactory.clientConnectionFailed(self, connector, reason)

    def sendMsg(self, m):
        self.server.sendMsg(m)

from twisted.internet import reactor
factory = EchoClientFactory()
reactor.connectTCP('localhost', 12345, factory)
msgs = ['/NAME fantasy', 'Hello, server!', 'How are you?', 'I would never to die! =P']
for m in msgs:
    reactor.callLater(0.5, factory.sendMsg, m)
reactor.run()

Теперь наш клиент будет висеть до тех пор, пока он сам не выйдет по Ctrl+C. И даже если сервер выключится, мы будет пытаться к нему подсоединиться. И даже строка \EXIT не поможет, т.к. она разрывает коннект с сервером по запросу сервера. Клиент этого не хочет, и поэтому он переподлючится к серверу.
Если такая функциональность не нужна, то можно воспользоваться стандартной фабрикой ClientFactory.

На этом все. В качестве тренировки (или если делать нечего), можно переписать код выше так, чтобы сервер запускался системной утилитой twistd. Заменить нужно будет всего-то одну строку.

PS: Отлов неточностей приветствуется.


8 комментариев:

  1. Блинчики с кровью — так себе. Кровь вкусная, а блинчики не очень.

    ОтветитьУдалить
  2. Грабли - ресторан. Му-му - кафе =)
    А ещё граблями клёво называть вокально-инструментальные ансамбли: "ВИА Грабля"

    ОтветитьУдалить
  3. Про протоколы я бы порекомендовал это: http://ru.wikipedia.org/wiki/Сетевая_модель_OSI

    Самые низкоуровневые протоколы - это вовсе не twisted.internet.protocol =)

    >> Протоколы физического уровня: IEEE 802.15 (Bluetooth), IRDA, EIA RS-232, EIA-422, ...

    ОтветитьУдалить
    Ответы
    1. Очевидно, что физические -- самые низкоуровневые. В данном случае, имелось в виду, именно протоколы прикладного уровня.

      Удалить
  4. Отличные кровавые блинчики! Уборная там выглядит, наверное, так. Предположения по поводу factory оставлю при себе.

    ОтветитьУдалить
    Ответы
    1. Чтож про уборную сказал, а про Factory умолчал? Все-все надо выкладывать.
      И кстати, да.. я не знаю как выглядит мужская уборная с писуарами )

      Удалить
    2. Извини, всё-всё выложить не могу. Впрочем оно и так вокруг и доступно, всегда-всегда. Всегда, пока есть, чему воспринимать.

      Уборная выглядит как-то так, только обычно без крупных пикселей посередине.

      Удалить