Search     or:     and:
 LINUX 
 Language 
 Kernel 
 Package 
 Book 
 Test 
 OS 
 Forum 
iakovlev.org

Server

Обзор

Twisted позволяет писать нагруженные серверы. В твистед есть несколько слоев.

Этот документ описывает т.н. Protocol слой. Речь пойдет о TCP, SSL и Unix socket servers, для UDP смотрите тут.

Ваш протокол будет управлять классом, производным от twisted.internet.protocol.Protocol. Большинство протоколов унаследованы от этого класса. Инстанс такого протокола может быть запущен при коннекте , или в любой произвольный момент.

Конфигурация поведения протокола прописывается в классе Factory, который унаследован от twisted.internet.protocol.Factory. По умолчанию фабрика - factory - запускает каждый протокол, и устанавливает ему атрибут, называемый factory, который указывает на себя.

Обычно это полезно, когда на разные порты или разные сетевые адреса привязываются разные сервисы. Factory ничего не знает про коннект и про сеть.

Протоколы

Протокол управляет данными асинхронно. Протокол никогда не ждет события, он их обрабатывает,когда они появляются.

Вот простой пример:

from twisted.internet.protocol import Protocol
 
 class Echo(Protocol):
 
     def dataReceived(self, data):
         self.transport.write(data)
 

Это простейший протокол. Он просто пишет назад то, что пишет ему клиент, и не отвечает ни на какие события. Вот пример протокола, отвечающего на события:

from twisted.internet.protocol import Protocol
 
 class QOTD(Protocol):
 
     def connectionMade(self):
         self.transport.write("An apple a day keeps the doctor away\r\n") 
         self.transport.loseConnection()
 

Этот протокол отвечает на коннект тем, что обрывает его.

Событие connectionMade происходит при коннекте. Событие connectionLost срабатывает при разрыве коннекта. Пример:

from twisted.internet.protocol import Protocol
 
 class Echo(Protocol):
 
     def connectionMade(self):
         self.factory.numProtocols = self.factory.numProtocols+1 
         if self.factory.numProtocols > 100:
             self.transport.write("Too many connections, try later") 
             self.transport.loseConnection()
 
     def connectionLost(self, reason):
         self.factory.numProtocols = self.factory.numProtocols-1
 
     def dataReceived(self, data):
         self.transport.write(data)
 

connectionMade и connectionLost работают с числом активных протоколов. connectionMade немедленно закрывает коннект при превышении квоты.

Использование Protocol

Вот пример кода сервера QOTD:

from twisted.internet.protocol import Protocol, Factory
 from twisted.internet import reactor
 
 class QOTD(Protocol):
 
     def connectionMade(self):
         self.transport.write("An apple a day keeps the doctor away\r\n") 
         self.transport.loseConnection()
 
 # Next lines are magic:
 factory = Factory()
 factory.protocol = QOTD
 
 # 8007 is the port you want to run under. Choose something >1024
 reactor.listenTCP(8007, factory)
 reactor.run()
 

Helper Protocols

Наиболее популярный интернет-протокол имеет line-based основу. Строки обычно заканчиваются комбинацией символов CR-LF.

Есть протоколы смешанные, у них есть как line-based секция, так и raw data секция, например HTTP/1.1 и Freenet протоколы.

Для этого в твистед есть LineReceiver протокол. У него есть два event handlers - lineReceived и rawDataReceived. По умолчанию, будет работать только lineReceived , один раз на каждую строку. Если вызвать setRawMode , протокол будет вызывать rawDataReceived до тех пор, пока будет вызываться setLineMode, который использует lineReceived.

Пример line receiver:

from twisted.protocols.basic import LineReceiver
 
 class Answer(LineReceiver):
 
     answers = {'How are you?': 'Fine', None : "I don't know what you mean"}
 
     def lineReceived(self, line):
         if self.answers.has_key(line):
             self.sendLine(self.answers[line])
         else:
             self.sendLine(self.answers[None])
 

Здесь разделитель не является частью строки.

Есть также протокол, основанный на netstring, и prefixed-message-length протокол.

State Machines

Во многих твистед протоколах хэндлерам нужно сохранять состояние.

  • Не нужно этим увлекаться. Записываемое состояние должно быть связано с одним уровнем абстракции.
  • Нужно использовать динамичность питона - см. пример SMTP client.
  • Не нужно смешивать код уровня приложения с кодом обработки событий протокола.

Factories

Базовым классом обычно является класс twisted.internet.protocol.Factory. Иногда возникает необходимость factory-specific конфигурации для протокола. В этих случаях нужно использовать класс Factory.

from twisted.internet.protocol import Factory
 from twisted.protocols.wire import Echo
 
 myFactory = Factory()
 myFactory.protocol = Echo
 

Для настройки конфигурации:

from twisted.internet.protocol import Factory, Protocol
 
 class QOTD(Protocol):
 
     def connectionMade(self):
         self.transport.write(self.factory.quote+'\r\n')
         self.transport.loseConnection()
 
 
 def makeQOTDFactory(quote=None):
     factory = Factory()
     factory.protocol = QOTD
     factory.quote = quote or 'An apple a day keeps the doctor away'
     return factory
 

Пример фабрики, которая позволяет протоколу писать лог файл:

from twisted.internet.protocol import Factory
 from twisted.protocols.basic import LineReceiver
 
 
 class LoggingProtocol(LineReceiver):
 
     def lineReceived(self, line):
         self.factory.fp.write(line+'\n')
 
 
 class LogfileFactory(Factory):
 
     protocol = LoggingProtocol
 
     def __init__(self, fileName):
         self.file = fileName
 
     def startFactory(self):
         self.fp = open(self.file, 'a')
 
     def stopFactory(self):
         self.fp.close()
 

Итог

А теперь конфигурируемый QOTD сервер :

from twisted.internet.protocol import Factory, Protocol
 from twisted.internet import reactor
 
 class QOTD(Protocol):
 
     def connectionMade(self):
         self.transport.write(self.factory.quote+'\r\n')
         self.transport.loseConnection()
 
 
 class QOTDFactory(Factory):
 
     protocol = QOTD
 
     def __init__(self, quote=None):
         self.quote = quote or 'An apple a day keeps the doctor away'
 
 reactor.listenTCP(8007, QOTDFactory("configurable quote"))
 reactor.run()
 

Client

Обзор

Твистед позволяет писать гибкие клиенты. Это достигается за счет нескольких слоев. В этом документе описываются клиенты TCP, SSL, Unix sockets

Протокол обычно унаследован от twisted.internet.protocol.Protocol. Большинство хэндлеров унаследовано от этого класса. Инстанс протокола создается при коннекте и удаляется при дисконнекте.

Конфигурация хранится в фабрике, которая обычно унаследована от twisted.internet.protocol.ClientFactory. Фабрика запускает протокол и устанавливает свой собственный атрибут. Это позволяет протоколу настраивать конфигурацию.

Protocol

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

Вот пример:

from twisted.internet.protocol import Protocol from sys import stdout class Echo(Protocol): def dataReceived(self, data): stdout.write(data)

Это простейший протокол, который просто печатает то, что читает. За кадром осталось еще много событий, на которые данный протокол просто не отвечает. Вот другой пример:

from twisted.internet.protocol import Protocol class WelcomeMessage(Protocol): def connectionMade(self): self.transport.write("Hello server, I am the client!\r\n") self.transport.loseConnection()

Этот протокол устанавливает коннект с сервером, посылает ему сообщение и разрывает коннект.

Событие connectionMade обычно происходит в момент старта протокола. Обьекты протокола заканчивают свою жизнь в connectionLost.

Одиночный клиент

Часто протоколу нужно только приконнектиться к серверу В этом случае класс twisted.internet.protocol.ClientCreator дает нужное API.

from twisted.internet import reactor from twisted.internet.protocol import Protocol, ClientCreator class Greeter(Protocol): def sendMessage(self, msg): self.transport.write("MESSAGE %s\n" % msg) def gotProtocol(p): p.sendMessage("Hello") reactor.callLater(1, p.sendMessage, "This is sent in a second") reactor.callLater(2, p.transport.loseConnection) c = ClientCreator(reactor, Greeter) c.connectTCP("localhost", 1234).addCallback(gotProtocol)

ClientFactory

Мы используем reactor.connect* и ClientFactory. ClientFactory - фабрика создающая протокол, она получает события в соответствии со статусом. Это позволяет фабрике делать реконнект к серверу в случае ошибки. Вот пример ClientFactory, использующий эхо-протокол и выводящий статус коннекта:

from twisted.internet.protocol import Protocol, ClientFactory from sys import stdout class Echo(Protocol): def dataReceived(self, data): stdout.write(data) class EchoClientFactory(ClientFactory): def startedConnecting(self, connector): print 'Started to connect.' def buildProtocol(self, addr): print 'Connected.' return Echo() def clientConnectionLost(self, connector, reason): print 'Lost connection. Reason:', reason def clientConnectionFailed(self, connector, reason): print 'Connection failed. Reason:', reason

При коннекте:

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

clientConnectionFailed вызывается, когда коннект не может быть установлен, clientConnectionLost вызывается после того, как коннект был сделан, а потом потерян.

Reconnection

Часто коннект теряется клиентом. При этом нужно вызвать connector.connect():

from twisted.internet.protocol import ClientFactory class EchoClientFactory(ClientFactory): def clientConnectionLost(self, connector, reason): connector.connect()

Коннектор передается в качестве первого аргумента в интерфейсе между коннектом и протоколом. Когда коннект теряется и фабрика получает событие clientConnectionLost , она делает вызов connector.connect() .

Для такой логики нужно имплементировать ReconnectingClientFactory , что позволить делать реконнект до тех пор , пока он не будет получен.

Вот пример ReconnectingClientFactory:

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)

A Пример: ircLogBot

Обзор ircLogBot

Пример посложнее можно найти в каталоге Twisted word doc/examples : examples

 

# twisted imports from twisted.words.protocols import irc from twisted.internet import reactor, protocol from twisted.python import log # system imports import time, sys class MessageLogger: """ An independent logger class (because separation of application and protocol logic is a good thing). """ def __init__(self, file): self.file = file def log(self, message): """Write a message to the file.""" timestamp = time.strftime("[%H:%M:%S]", time.localtime(time.time())) self.file.write('%s %s\n' % (timestamp, message)) self.file.flush() def close(self): self.file.close() class LogBot(irc.IRCClient): """A logging IRC bot.""" nickname = "twistedbot" def connectionMade(self): irc.IRCClient.connectionMade(self) self.logger = MessageLogger(open(self.factory.filename, "a")) self.logger.log("[connected at %s]" % time.asctime(time.localtime(time.time()))) def connectionLost(self, reason): irc.IRCClient.connectionLost(self, reason) self.logger.log("[disconnected at %s]" % time.asctime(time.localtime(time.time()))) self.logger.close() # callbacks for events def signedOn(self): """Called when bot has succesfully signed on to server.""" self.join(self.factory.channel) def joined(self, channel): """This will get called when the bot joins the channel.""" self.logger.log("[I have joined %s]" % channel) def privmsg(self, user, channel, msg): """This will get called when the bot receives a message.""" user = user.split('!', 1)[0] self.logger.log("<%s> %s" % (user, msg)) # Check to see if they're sending me a private message if channel == self.nickname: msg = "It isn't nice to whisper! Play nice with the group." self.msg(user, msg) return # Otherwise check to see if it is a message directed at me if msg.startswith(self.nickname + ":"): msg = "%s: I am a log bot" % user self.msg(channel, msg) self.logger.log("<%s> %s" % (self.nickname, msg)) def action(self, user, channel, msg): """This will get called when the bot sees someone do an action.""" user = user.split('!', 1)[0] self.logger.log("* %s %s" % (user, msg)) # irc callbacks def irc_NICK(self, prefix, params): """Called when an IRC user changes their nickname.""" old_nick = prefix.split('!')[0] new_nick = params[0] self.logger.log("%s is now known as %s" % (old_nick, new_nick)) # For fun, override the method that determines how a nickname is changed on # collisions. The default method appends an underscore. def alterCollidedNick(self, nickname): """ Generate an altered version of a nickname that caused a collision in an effort to create an unused related name for subsequent registration. """ return nickname + '^' class LogBotFactory(protocol.ClientFactory): """A factory for LogBots. A new protocol instance will be created each time we connect to the server. """ # the class of the protocol to build when new connection is made protocol = LogBot def __init__(self, channel, filename): self.channel = channel self.filename = filename def clientConnectionLost(self, connector, reason): """If we get disconnected, reconnect to server.""" connector.connect() def clientConnectionFailed(self, connector, reason): print "connection failed:", reason reactor.stop() if __name__ == '__main__': # initialize logging log.startLogging(sys.stdout) # create factory protocol and application f = LogBotFactory(sys.argv[1], sys.argv[2]) # connect factory to this host and port reactor.connectTCP("irc.freenode.net", 6667, f) # run bot reactor.run()

ircLogBot.py коннектится к IRC server, присоединяет канал, и логирует весь трафик в файл. Используется реконнект при потере.

Persistent Data in the Factory

Поскольку протокол каждый раз при коннекте создается заново, клиенту нужно сохранять данные . В данном случае нужно помнить о том, какой irc-канал логируется, и куда ведется логирование.

from twisted.internet import protocol from twisted.protocols import irc class LogBot(irc.IRCClient): def connectionMade(self): irc.IRCClient.connectionMade(self) self.logger = MessageLogger(open(self.factory.filename, "a")) self.logger.log("[connected at %s]" % time.asctime(time.localtime(time.time()))) def signedOn(self): self.join(self.factory.channel) class LogBotFactory(protocol.ClientFactory): protocol = LogBot def __init__(self, channel, filename): self.channel = channel self.filename = filename

Когда протокол создается, он получает ссылку на фабрику как self.factory.

Что еще почитать

Класс Protocol используется как имплементация IProtocol. Для изучения интерфейса IProtocol , смотрите API documentation IProtocol.

Атрибут transport унаследован от интерфейса ITCPTransport. Смотрите API documentation для ITCPTransport.

По интерфейсам также смотрите Components: Interfaces and Adapters.

Эхо-сервер

Код простого эхо-сервера в твистед получается компактным:
 from twisted.internet import reactor
 from twisted.internet.protocol import ServerFactory 
 from twisted.protocols.basic import LineOnlyReceiver 
 
 class ChatProtocol(LineOnlyReceiver): 
 
     name = "" 
 
     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 my chat server.") 
         self.sendLine("Send '/NAME [new name]' to change your name.") 
         self.sendLine("Send '/EXIT' to quit.") 
         self.factory.sendMessageToAllClients(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.sendMessageToAllClients(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.sendMessageToAllClients(oldName+" changed name to "+self.getName()) 
         elif line=="/EXIT": 
             self.transport.loseConnection() 
         else: 
             self.factory.sendMessageToAllClients(self.getName()+" says "+line) 
 
     def sendLine(self, line): 
         self.transport.write(line+"\r\n") 
 
 class ChatProtocolFactory(ServerFactory): 
 
     protocol = ChatProtocol 
 
     def __init__(self): 
         self.clientProtocols = [] 
 
     def sendMessageToAllClients(self, mesg): 
         for client in self.clientProtocols:
             client.sendLine(mesg) 
 
 print "Starting Server"
 factory = ChatProtocolFactory()
 reactor.listenTCP(12345, factory)
 reactor.run() 
 
В качестве клиента можно использовать телнет:
 >> telnet localhost 12345
 
При коннекте каждый клиент будет создавать на сервере свой экземпляр протокола. При этом клиент внутри протокола ничего не знает о других клиентах. Где хранить клиентов? Мы для этого используем фабрику:
     def connectionMade(self): 
         ...
         self.factory.clientProtocols.append(self)
 
Добавим в фабрику атрибут - список клиентов clientProtocols: в конструкторе фабрики мы этот список и определим:
     def __init__(self): 
         self.clientProtocols = [] 
 
Также фабрика будет заниматься широковещательной рассылкой каждого сообщения от каждого клиента:
 def sendMessageToAllClients(self, mesg): 
         for client in self.clientProtocols:
             client.sendLine(mesg)
 

Оставьте свой комментарий !

Ваше имя:
Комментарий:
Оба поля являются обязательными

 Автор  Комментарий к данной статье