![image](https://habrastorage.org/storage/habraeffect/8c/96/8c9627fb7b70140763c3075a78deb13f.jpg)
- Облегчить сервер от ненужной работы, например отрисовки самой странички, используя вместо этого javascript шаблонизатор.
- Использовать хороший front-end, например nginx, учитывая пункт первый, динамики у нас нет, и это нам вполне подходит.
- Распределяя нагрузку на frontend, например используя Tornado.
Остался самый главный вопрос, что будет происходить когда пользователь совершает какое-либо действие? Обычные ajax запросы не подойдут, вполне понятно почему. Поэтому нам на помощь приходят сокеты.
Предыстоия
Игра построена не на флеше, а js не умеет работать с сокетами, поэтому можно использовать флеш подложку которая займется этим вопросом. Тут нам поможет небольшая библиотечка jsocket.
Для сервера первым делом внимание пало на Twisted, и уже даже начал кое что писать, как обнаружил для себя еще целую гору интересных инструментов, из которых больше всего приглянулись gevent и tornado. Поискав информацию про каждый из них обнаружил интересную статью, а позже эту. Однако там рассматривается немного другая задача, нежели нужна мне, поэтому я решил провести свое тестирование.
Скрипты
Пишем простой клиент для тестов на twisted.
from twisted.internet.protocol import ClientFactory
from twisted.protocols.basic import LineReceiver
from twisted.internet import epollreactor
epollreactor.install()
from twisted.internet import reactor
import sys,time
clients=30000
host='127.0.0.1'
port=8001
file = open( 'log.dat', 'w')
class glob():
connections=0
crefuse=0
clost=0
def enchant(self):
self.connections+=1
def refuse(self):
self.crefuse+=1
def lost(self):
self.clost+=1
a=glob()
class EchoClient(LineReceiver):
measure=True
def connectionMade(self):
self.sendLine("Hello, world!")
self.t1 = time.time()
def lineReceived(self, line):
if self.measure:
self.t2 = time.time() - self.t1
file.write('%s %s %s %s %s\n' % (a.connections+1,self.t2,a.crefuse,a.clost,line))
self.measure=False
if a.connections+1 < clients:
a.enchant()
reactor.connectTCP(host, port, EchoClientFactory())
else:
self.transport.loseConnection()
class EchoClientFactory(ClientFactory):
protocol = EchoClient
def clientConnectionFailed(self, connector, reason):
a.refuse()
def clientConnectionLost(self, connector, reason):
a.lost()
def main():
f = EchoClientFactory()
reactor.connectTCP(host, port, f)
reactor.run()
file.close()
if __name__ == '__main__':
main()
И сервер на каждом фреймворке.
gevent
clients=[]
host=''
port = 8001
def echo(socket, address):
clients.append(socket)
while True:
line = socket.recv(1024)
for client in clients:
try:
client.send(str(len(clients))+'\r\n')
except:
clients.remove(client)
if __name__ == '__main__':
from gevent.server import StreamServer
StreamServer((host, port), echo).serve_forever()
tornado
import errno
import functools
import socket
from tornado import ioloop, iostream
host=''
port = 8001
clients=[]
class Connection(object):
def __init__(self, connection):
clients.append(self)
self.stream = iostream.IOStream(connection)
self.read()
def read(self):
self.stream.read_until('\r\n', self.eol_callback)
def eol_callback(self, data):
for c in clients:
try:
c.stream.write(str(len(clients))+'\r\n')
except:
clients.remove(c)
self.read()
def connection_ready(sock, fd, events):
while True:
try:
connection, address = sock.accept()
except socket.error, e:
if e[0] not in (errno.EWOULDBLOCK, errno.EAGAIN):
raise
return
connection.setblocking(0)
Connection(connection)
if __name__ == '__main__':
sock = socket.socket(socket.AF_INET, socket.SOCK_STREAM, 0)
sock.setsockopt(socket.SOL_SOCKET, socket.SO_REUSEADDR, 1)
sock.setblocking(0)
sock.bind((host, port))
sock.listen(30000)
io_loop = ioloop.IOLoop.instance()
callback = functools.partial(connection_ready, sock)
io_loop.add_handler(sock.fileno(), callback, io_loop.READ)
try:
io_loop.start()
except KeyboardInterrupt:
io_loop.stop()
print "exited cleanly"
twisted
from twisted.protocols import basic
class MyChat(basic.LineReceiver):
def connectionMade(self):
self.factory.clients.append(self)
def connectionLost(self, reason):
self.factory.clients.remove(self)
def dataReceived(self, line):
for c in self.factory.clients:
c.message(str(len(factory.clients))+'\r\n')
def message(self, message):
self.transport.write(message)
from twisted.internet import epollreactor
epollreactor.install()
from twisted.internet import reactor, protocol
from twisted.application import service, internet
factory = protocol.ServerFactory()
factory.protocol = MyChat
factory.clients = []
reactor.listenTCP(8001,factory)
reactor.run()
Небольшое пояснение
По сути мы создали чат сервер, Как только сервер получает строку от клиента, он рассылает эту строку всем подключенным клиентам (точнее вместо этого мы рассылаем количество подключенных клиентов). Таким образом нагрузка возрастает арифметически. Клиент в свою очередь измеряет время прошедшее между отправкой сообщения на сервер и получением ответа от него. Так как наш текущий клиент подключился последним, то все клиенты к этому моменту уже получат ответ.
При измерениях я снимал зависимость времени ответа от количества подключенных пользователей. Клиент и сервер запускались на локалхосте. Машина при этом используется как десктоп, так что результаты могут быть немного искажены, однако суть при этом ясна. Планировалось измерить зависимость вплоть до 30000 подлючений, но к сожалению для всех трех фреймворков это оказалось слишком долго.
![image](http://habrastorage.org/storage/habraeffect/d2/80/d28099092b240ec021aacbf323232f60.jpg)
По оси отклика, время, разумеется, в секундах.
На графике у tornado видно две линии. Это не два теста, а один, просто результаты получились такого вида:
11476 2.45670819283
11477 0.405035018921
11478 2.42619085312
11479 0.392680883408
11480 2.5216550827
11481 0.401995897293
где первое число это количество коннектов, а второе время ответа. Я не знаю с чем это связано.
Выводы
Привожу список всех плюсов и минусов, в рамках поставленной задачи
Gevent
- + Высокая скорость работы.
- + Лаконичный код.
- — Не очень удобно.
Twisted
- + Понятный код.
- + Множество дополнительных функций.
- + Обширная документация.
- — Тормознутый домашний сайт.
- — Медленный.
Tornado
- + С ним быстрее всего получилось разобраться.
- — Нет документации
- — Не очень удобно.
- — Нестабильное время отклика.
Что-ж из всего вышесказаного, каждый сможет сделать выводы для себя сам. Я для себя и своих задач выберу gevent.
UPD. немного ошибся со скриптом на gevent. На него было меньше нагрузки. Перемерил, результат получился немного хуже, однако остался по прежнему лучшим. вот примерные результаты:
10336 1.01536607742 0 0 10338
10337 0.955881118774 0 0 10339
10338 0.947958946228 0 0 10340
10339 1.02578997612 0 0 10341