Pull to refresh

Асинхронный удар

Python *
Sandbox
imageКак уже наверное кто-то догадался, в этой статье речь пойдет о сокетах, и фреймфорках облегчающих работу с ними. Недавно я начал работу надо новым проектом, онлайн игрой. Для таких проектов довольно критично время ответа от сервера, если это конечно не пошаговая стратегия, хотя и в этом случае пожалуй тоже. Так как же этого добиться при суровой ограниченности ресурсов?
  • Облегчить сервер от ненужной работы, например отрисовки самой странички, используя вместо этого 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
По оси отклика, время, разумеется, в секундах.
На графике у 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
Tags:
Hubs:
Total votes 52: ↑47 and ↓5 +42
Views 16K
Comments Comments 59