Оглавление
  • Введение
  • Структура
  • Routes
  • Handlers, Request and Response
  • Настройки конфигурации
  • Middlewares
  • Базы данных
  • Шаблоны
  • Сессии, авторизация
  • Static
  • WebSocket
  • Выгрузка на Heroku



Введение


Прошлой осенью мне удалось побывать на нескольких python meetups в Киеве.
На одном из них выступал Николай Новик и рассказывал о новом асинхронном фреймворке aiohttp, работающем на библиотеке для асинхронных вызовов asyncio в 3 версии интерпретатора питона. Данный фреймворк заинтересовал меня тем, что он создавался core python разработчиками и позиционировался как концепт python фреймворка для веба.


Сейчас имеется огромное количество разных фреймворков, в каждом из которых своя философия,
синтаксис и реализация общих для веба шаблонов. Надеюсь, что со временем, все это разнообразие
будет на одной основе — aiohttp.


Структура


Чтобы протестировать по максимуму все возможности aiohttp, я попытался разработать простой чат на вебсокетах. Основой aiohttp является бесконечный loop, в котором крутятся handlers. Handler — так называемая coroutine, объект, который не блокирует ввод/вывод(I/O). Данный тип объектов появился в python 3.4 в библиотеке asyncio. Пока не произойдут все вычисления в данном объекте, он как бы засыпает, а в это время интерпретатор может обрабатывать другие объекты. Чтобы было понятно, приведу пример. Зачастую все задержки сервера происходят, когда он ожидает ответа от базы данных и пока этот ответ не придёт и не обработается, другие объекты ждут своей очереди. В данном случае другие объекты будут обрабатываться, пока не придёт ответ из базы. Но для реализации этого нужен асинхронный драйвер.
На данный момент для aiohttp реализованы асинхронные драйвера и обёртки для большинства популярных баз данных (postgresql, mysql, redis)
Для mongodb есть Motor, который используется в чате.


Точкой входа для чата служит файл app.py. В нем создаётся объект app.


import asyncio
from aiohttp import web

loop = asyncio.get_event_loop()

app = web.Application(loop=loop, middlewares=[
    session_middleware(EncryptedCookieStorage(SECRET_KEY)),
    authorize,
    db_handler,
])

Как вы видите, при инициализации в app передаётся loop, а также список middleware, о котором будет рассказано попозже.


Routes


В отличии от flask на который aiohttp очень похож, routes добавляются в уже инициализированное приложение app.


app.router.add_route('GET', '/{name}', handler)

Вот кстати объяснение Андрея Светлова, почему именно так реализовано.


Заполнение routes вынесено в отдельный файл routes.py.


from chat.views import ChatList, WebSocket
from auth.views import Login, SignIn, SignOut

routes = [
    ('GET', '/',        ChatList,  'main'),
    ('GET', '/ws',      WebSocket, 'chat'),
    ('*',   '/login',   Login,     'login'),
    ('*',   '/signin',  SignIn,    'signin'),
    ('*',   '/signout', SignOut,   'signout'),
]

Первый элемент — http метод, далее расположен url, третьим в кортеже идёт объект handler, и напоследок — имя route, чтобы удобно было его вызывать в коде.


Далее импортируется список routes в app.py и они заполняются простым циклом в приложение.


from routes import routes

for route in routes:
        app.router.add_route(route[0], route[1], route[2], name=route[3])

Все просто и логично


Handlers, Request and Response


Я решил обработку запросов сделать по примеру Django фреймворка. В папке auth находится все, что касается пользователей, авторизации, обработка создания пользователя и его входа. А в папке chat находится логика работы чата соответственно. В aiohttp можно реализовать handler в качестве как функции, так и класса.
Выбираем реализацию через класс.


class Login(web.View):

    async def get(self):
        session = await get_session(self.request)
        if session.get('user'):
            url = request.app.router['main'].url()
            raise web.HTTPFound(url)
        return b'Please enter login or email'

Про сессии будет написано ниже, а все остальное думаю понятно и так. Хочу заметить, что переадресация происходит либо возвратом(return) либо выбросом исключения в виде объекта web.HTTPFound(), которому передаётся путь параметром. Http методы в классе реализуются через асинхронные функции get, post и тд. Есть некоторые особенности, если нужно работать с параметрами запроса.


data = await self.request.post()

Настройки конфигурации


Все настройки хранятся в файле settings.py. Для хранения секретных данных я использую envparse. Данная утилита позволяет читать данные из переменных окружения, а также парсить специальный файл, где эти переменные хранятся.


if isfile('.env'):
    env.read_envfile('.env')

Во первых, это было необходимо для поднятия проекта на Heroku, а во вторых, это оказалось ещё и очень удобно. Сначала я использовал локальную базу, а потом тестировал на удалённой и переключение состояло из изменения всего одной строки в файле .env.


Middlewares


При инициализации приложения можно задавать middleware. Здесь они вынесены в отдельный файл. Реализация стандартная — функция декоратор, в которой можно делать проверки или любые другие действия с запросом.


Пример проверки на авторизацию


async def authorize(app, handler):
    async def middleware(request):
        def check_path(path):
            result = True
            for r in ['/login', '/static/', '/signin', '/signout', '/_debugtoolbar/']:
                if path.startswith(r):
                    result = False
            return result

        session = await get_session(request)
        if session.get("user"):
            return await handler(request)
        elif check_path(request.path):
            url = request.app.router['login'].url()
            raise web.HTTPFound(url)
            return handler(request)
        else:
            return await handler(request)

    return middleware

Также есть middleware для подключения базы данных.


async def db_handler(app, handler):
    async def middleware(request):
        if request.path.startswith('/static/') or request.path.startswith('/_debugtoolbar'):
            response = await handler(request)
            return response

        request.db = app.db
        response = await handler(request)
        return response
    return middleware

Детали подключения ниже по тексту.


Базы данных


Для чата используется Mongodb и асинхронный драйвер Motor. Подключение к базе происходит при инициализации приложения.


app.client = ma.AsyncIOMotorClient(MONGO_HOST)
app.db = app.client[MONGO_DB_NAME]

А закрытие соединения происходит в специальной функции shutdown.


async def shutdown(server, app, handler):

    server.close()
    await server.wait_closed()
    app.client.close()  # database connection close
    await app.shutdown()
    await handler.finish_connections(10.0)
    await app.cleanup()

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


Немного подробнее про создание event loop.


loop = asyncio.get_event_loop()
serv_generator, handler, app = loop.run_until_complete(init(loop))
serv = loop.run_until_complete(serv_generator)
log.debug('start server', serv.sockets[0].getsockname())
try:
    loop.run_forever()
except KeyboardInterrupt:
    log.debug(' Stop server begin')
finally:
    loop.run_until_complete(shutdown(serv, app, handler))
    loop.close()
log.debug('Stop server end')

Сам loop создаётся из asyncio.


serv_generator, handler, app = loop.run_until_complete(init(loop))

Метод run_until_complete добавляет corutines в loop. В данном случае он добавляет функцию инициализации приложения.


try:
    loop.run_forever()
except KeyboardInterrupt:
    log.debug(' Stop server begin')
finally:
    loop.run_until_complete(shutdown(serv, app, handler))
    loop.close()

Собственно сама реализация бесконечного цикла, который прерывается в случае исключения. Перед закрытием вызывается функция shutdown, которая завершает все соединения и корректно останавливает сервер.


Теперь нам надо разобраться, как делать запросы, извлекать и изменять данные


class Message():

    def __init__(self, db, **kwargs):
        self.collection = db[MESSAGE_COLLECTION]

    async def save(self, user, msg, **kw):
        result = await self.collection.insert({'user': user, 'msg': msg, 'time': datetime.now()})
        return result

    async def get_messages(self):
        messages = self.collection.find().sort([('time', 1)])
        return await messages.to_list(length=None)

Хотя у меня не задействована ОРМ, запросы к базе удобнее делать в отдельных классах. В папке chat был создан файл models.py, где находится класс Message. В методе get_messages создаётся запрос, который достаёт все сохранённые сообщения, отсортированные по времени. В методе save создаётся запрос на сохранение сообщения в базу.


Шаблоны


Для aiohttp написано несколько асинхронных обёрток для популярных шаблонизаторов, в частности aiohttp_jinja2 и aiohttp_mako. Для чата использую jinja2.


aiohttp_jinja2.setup(app, loader=jinja2.FileSystemLoader('templates'))

Вот так поддержка шаблонов инициализируется в приложении.
FileSystemLoader('templates') указывает jinja2 что наши шаблоны лежать в папке templates.


class ChatList(web.View):
    @aiohttp_jinja2.template('chat/index.html')
    async def get(self):
        message = Message(self.request.db)
        messages = await message.get_messages()
        return {'messages': messages}

Через декоратор мы указываем, какой шаблон будем использовать во views, а для заполнения контекста, возвращаем словарь с переменными, с которыми потом работаем в шаблоне.


Сессии, авторизация


Для работы с сессиями есть библиотека aiohttp_session. Есть возможность хранить сессии в Redis или в cookies в зашифрованном виде, используя cryptography. Способ хранения указывается ещё при установке библиотеки.


aiohttp_session[secure]

Для инициализации сессии, добавляем её в middleware.


session_middleware(EncryptedCookieStorage(SECRET_KEY)),

Чтобы достать или положить значения в сессию, нужно сначала извлечь её из запроса.


session = await get_session(request)

Для авторизации пользователя, добавляем в сессию его id, а потом в middleware проверяем его наличие. Конечно для безопасности нужно больше проверок, но для тестирования концепции хватит и этого.


Static


Папка с статическим контентом подключается отдельным route при инициализации приложения.


app.router.add_static('/static', 'static', name='static')

Чтобы задействовать её в шаблоне, нужно достать её из app.


<script src="{{ app.router.static.url(filename='js/main.js') }}"></script>

Все просто, ничего сложного нету.


WebSocket


Наконец-то мы добрались до самой вкусной части aiohttp). Реализация socket очень проста. В javascript я добавил минимально необходимую функциональность для его работы.


try{
    var sock = new WebSocket('ws://' + window.location.host + '/ws');
}
catch(err){
    var sock = new WebSocket('wss://' + window.location.host + '/ws');
}

// show message in div#subscribe
function showMessage(message) {
    var messageElem = $('#subscribe'),
        height = 0,
        date = new Date();
        options = {hour12: false};
    messageElem.append($('<p>').html('[' + date.toLocaleTimeString('en-US', options) + '] ' + message + '\n'));
    messageElem.find('p').each(function(i, value){
        height += parseInt($(this).height());
    });

    messageElem.animate({scrollTop: height});
}

function sendMessage(){
    var msg = $('#message');
    sock.send(msg.val());
    msg.val('').focus();
}

sock.onopen = function(){
    showMessage('Connection to server started')
}

// send message from form
$('#submit').click(function() {
    sendMessage();
});

$('#message').keyup(function(e){
    if(e.keyCode == 13){
        sendMessage();
    }
});

// income message handler
sock.onmessage = function(event) {
  showMessage(event.data);
};

$('#signout').click(function(){
    window.location.href = "signout"
});

sock.onclose = function(event){
    if(event.wasClean){
        showMessage('Clean connection end')
    }else{
        showMessage('Connection broken')
    }
};

sock.onerror = function(error){
    showMessage(error);
}

Для реализации серверной части я использую class WebSocket


class WebSocket(web.View):
    async def get(self):
        ws = web.WebSocketResponse()
        await ws.prepare(self.request)

        session = await get_session(self.request)
        user = User(self.request.db, {'id': session.get('user')})
        login = await user.get_login()

        for _ws in self.request.app['websockets']:
            _ws.send_str('%s joined' % login)
        self.request.app['websockets'].append(ws)

        async for msg in ws:
            if msg.tp == MsgType.text:
                if msg.data == 'close':
                    await ws.close()
                else:
                    message = Message(self.request.db)
                    result = await message.save(user=login, msg=msg.data)
                    log.debug(result)
                    for _ws in self.request.app['websockets']:
                        _ws.send_str('(%s) %s' % (login, msg.data))
            elif msg.tp == MsgType.error:
                log.debug('ws connection closed with exception %s' % ws.exception())

        self.request.app['websockets'].remove(ws)
        for _ws in self.request.app['websockets']:
            _ws.send_str('%s disconected' % login)
        log.debug('websocket connection closed')

        return ws

Сам socket создаётся используя функцию WebSocketResponse(). Обязательно перед использованием его нужно "приготовить". Список открытых sockets у меня хранится в приложении(чтобы при закрытии сервера их можно было корректно закрыть). При подключении нового пользователя, все участники получают уведомление о том что новый участник присоединился к чату. Далее мы ожидаем сообщения от пользователя. Если оно валидно, мы сохраняем его в базе данных и отсылаем другим участникам чата.
Когда socket закрывается, мы удаляем его из списка и оповещаем чат, что его покинул один из участников. Очень простая реализация, визуально в синхронном стиле, без большого количества callbacks, как в Tornado к примеру. Бери и пользуйся).


Выгрузка на Heroku


Тестовый чат я выложил на Heroku, для наглядной демонстрации. При установке возникло несколько проблем, в частности для использования их внутренней базы mongodb нужно было вносить данные кредитной карты, что делать мне не хотелось, поэтому воспользовался услугами MongoLab и создал там базу. Далее были проблемы с установкой самого приложения. Для установки cryptography нужно было явно указывать его в requirements.txt. Также для указания версии python нужно создавать в корне проекта файл runtime.txt.


Выводы


В целом создание чата, изучение aiohttp, разбор работы sockets и некоторых других технологий, с которыми я до этого не работал, заняло у меня где-то около 3 недель работы по вечерам и редко на выходных.
Документация в aiohttp довольно неплохая, много асинхронных драйверов и обёрток уже готовы для тестирования.
Возможно для production пока не все готово, но развитие идёт очень активно (за 3 недели aiohttp обновилась с версии 0.19 до 0.21).
Если нужно добавить в проект sockets, этот вариант отлично подойдёт, чтобы не добавлять тяжёлую Tornado в зависимости.


Ссылки



Все ошибки и недочеты присылайте пожалуйста в личку :)

Поделиться с друзьями
-->

Комментарии (28)


  1. vladkozlovski
    17.05.2016 03:04
    +3

    Фреймворк крутой, у меня на нём уже около 10 проектов работают. Единственное, что меня больше всего печалит, это драйвера к БД, а точнее их отсутствие. Если БД не очень распространённая, то драйвера точно не будет, например какой-нибудь Aerospike. Когда драйвер есть, то отстающий в развитии с кучей непонятных зависимостей. Пример Motor, который тянет за собой старую pymongo.

    В итоге надо всегда думать и планировать, что вы будете использовать в проекте, а что нет, что бы не случилась беда. А с учётом того, как долго все переезжали на Python 3 (до сих пор новые проекты встречаю на Python 2.7), непонятно как долго ждать чуда.

    Также немаловажным является возможность запуска кода на PyPy, особенно когда речь о каких-то нагрузках (фреймворк же для этих целей). Здесь, в отличии от того же Tornado, так не получится.

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


  1. splatt
    17.05.2016 04:16
    +3

    Пробывали писать на aiohttp, и… не получилось. В определенный момент пришлось переписать все на стандартном стэке Flask + SQLAlchemy.
    Aiohttp — хороший фреймворк для написания простых и плоских (flat) приложений (вроде todo-листа или веб-чата), но когда дело доходит до написания серьезных приложений, насыщенных ООП и с хоть какой-нибудь вложенностью, то делать это крайне тяжело. Поскольку любые функции для работы с БД должны быть корутинами, то и функции, вызывающие их, так же должны быть корутинами. В результате весь код превращается в одну большую корутину, со всеми вытекающими последствиями.

    Ну об использовании ORM-фреймворков не может идти и речи, поскольку все они завязаны на динамике языке и использовании динамических @property и lazy-подгрузке данных из БД. Asyncio-проперти язык поддерживает, но это просто АДъ и реально работать с таким кодом нереально.
    В результате выхода два — либо писать sql-запросы прямо в коде (приехали), либо вообще не использовать реляционных БД и писать на том же Mongo (возвращаемся к вопросу о том, какие приложения на таком стэке можно реально написать).


    1. Crandel
      17.05.2016 06:55

      Недавно был PyConUa во Львове и на одном из докладов, посвященых aiohttp, я услышал про peewee-async https://github.com/05bit/peewee-async. Но сам не пробовал ещё использовать. Отсутствие Sqlalchemy-orm не позволяет использовать aiohttp на продакшене


    1. Crandel
      17.05.2016 07:05

      del


    1. lega
      17.05.2016 08:05

      В результате весь код превращается в одну большую корутину, со всеми вытекающими последствиями.
      Это должен пережить каждый разработчик, т.к. не верит что асинхронный подход (в питоне) не для всех задач, а лучше сказать — для узкого круга. Кроме того он может быть гораздо медленее.
      Вот чаты и подобные задачи с долгими соединениями на нем хорошо делать.


    1. Tirael78
      17.05.2016 09:58

      Работаю с aiohttp года полтора, с предрелизной версии. Сейчас реализовано несколько серьезных проектов.
      Да, с корутинами такая история, если вы пишите проект с нуля, то это не проблема. Исправлять уже рабочий проект будет сложно.
      По asyncio отличная документация есть. И нужно просто немного разобраться.
      Писать сложные и нагруженные проекты на asyncio, в том числе используя aiohttp, можно и нужно, и на самом деле не так уж все и сложно.
      Что касается наличия асинхронных интерфейсов, то сейчас их достаточно много, и регулярно новые появляются, вообще для собственной реализации можно и свой написать, это тоже не очень сложно.

      Что касается ORM, да, в настоящее время продуктивно использовать их не выйдет, только в высоко нагруженных системах их используют не часто.

      Важно понять, что за асинхронностью — будущее, вот и uvloop появился, это огромный прирос по производительности.
      Просто надо сесть, почитать документацию, и нет проблем.


      1. lega
        17.05.2016 11:06

        Важно понять, что за асинхронностью — будущее
        Это где так мозги промывают?
        Асинхронный код (речь не про асинхронное выполнение кода) — это шаг назад, когда уже есть «корутины», gevent, fibers, это есть и в python, и в node.js и GoLang и много где.


      1. splatt
        18.05.2016 16:49
        -1

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


        Поясните, что вы имеете ввиду, когда говорите «сложные»? Про высоконагруженные я ничего и не говорю — да, скорее всего, это основное предназначение aiohttp. Приложение вроде Tinder может быть крайне высоконагруженным, но сложным его назвать язык не поворачивается (пару endpoint'ов и mongo-коллекций вроде user, swipe, match… ну message еще, хотя для swipe даже отдельной коллекции не нужно).

        Как написать какой-нибудь форум или тот-же хабрахабр… да в общем, любой проект где вам может потребоваться сделать доп. итерации и ввести дополнительный функционал (добавить тэги, комментарии, панель администрирования с различными правами, итд) — я не очень представляю как это сделать без полноценной ORM системы.


        1. Tirael78
          18.05.2016 17:09
          +1

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


          1. splatt
            19.05.2016 02:47

            Поэтому я и попросил пояснить. Мне интересно, что Вы называете «сложными проектами», раз вы говорите, что «Писать сложные и нагруженные проекты на asyncio, в том числе используя aiohttp, можно и нужно». Я не собираюсь с вами спорить, но мне интересно понять в чем для вас заключается «сложный проект».


  1. excentro
    17.05.2016 07:07

    Расстраивает, что от uvloop нет практически никакого ускорения работы aiohttp, на что я очень надеялся.


    1. mirror13
      17.05.2016 07:59

      Я вот тоже не заметил положительного влияния uvloop на aiohttp. Но вполне возможно, что оно проявится при действительно высокой нагрузке на сервис.


    1. Tirael78
      17.05.2016 10:00

      Для этого нужно внести изменения в aiohttp, в парсер, например.
      просто надо дождаться нового релиза от Светлова

      на aiohttp асинхронность в python не заканчивается, и uvloop дает очень заметный прирост производительности, если вы правильно его используете.


      1. excentro
        17.05.2016 10:53
        +1

        Это понятно. Но именно по aiohttp многие судят о скорости работы питона (применительно к вебу, конечно). Посмотрите на тесты фреймворков.


  1. themtrx
    17.05.2016 07:53
    -2

    Глас скорбящей души. Результаты поиска по заказам в Upwork без дополнительных фильтров.
    Ключевое слово «python»: 1,575 jobs found
    Ключевое слово «javascript»: 2,594 jobs found

    Python, 13 сентября 2015:
    async def doMe(x, y, z):
    k = await x.atata(y, z)
    print(k)

    Javascript, 17 мая 2016:
    let doMe = (x, y, z) => {
    (x.atata(y, z, (k) => {
    console.log(k);
    } || x.atata(y, z).then((k) => {
    console.log(k);
    )}.catch((e) => { console.log('byaka ' + k);
    }));
    };

    Эх… Однажды и на нашей улице будет праздник.


    1. Crandel
      17.05.2016 09:21
      +1

      Я думаю что доля серверного Javascript не такая уж и большая. В основном фронтэнд


      1. vladkozlovski
        17.05.2016 12:43

        А я не думаю, я уверен ;)


      1. themtrx
        17.05.2016 15:17
        -2

        Ключевое слово «node.js»: 788 jobs found
        Ключевое слово «angular»: 1,044 jobs found (как никак без серверсайда angular не пишут)
        Ключевое слово «node»: 1,171 jobs found

        Естественно, некорректно было бы суммировать эти показатели, но вот среднее арифметическое — 1183 — вполне сопоставимо с числом, которое получим, если на глаз вычтем из показателей по Python несерверсайд (всякие скраперы, десктоп, фаст-шел-скрипты и прочее).

        Однако же, разве я говорил о серверсайде? Я привёл в сравнение конкретно два языка и глас скорбящей души о том, что в Javascript, который, как мне кажется, используется несколько более широко, чем Python, всё ещё нет адекватного async/await синтаксиса, хотя спецификация наличествует (ES7). Не более того.


        1. Crandel
          17.05.2016 15:26

          Во первых, angular на фронте и никаким образом до серверной части не относится. Во вторых javascript используется почти исключительно при веб разработке, а python — язык основного назначения(веб, Биг дата и тд.) и сравнивать тут совершенно некорректно


          1. themtrx
            17.05.2016 15:56
            -2

            Вы поставили мне минус, не разбираясь в вопросе. Уж простите, но утверждать, будто angular на фронте — это демонстрация полного незнания темы. Модели и контроллеры, сценарии и вообще структура приложения, всё это на фронте? «yo angular», по Вашему, клиентский Javascript генерирует, чтоли? Буду честен, хотел бы Вам поставить за дерзкий пустой понт минус, но, увы, я пока лишь падаван и сила хабраджедая мне недоступна.

            Мой изначальный комментарий о том, что в Python есть адекватный async/await, а в Javascript — нет, независимо от того, что и где используется, и о том, что мне от сего факта грустно.


            1. Crandel
              17.05.2016 16:01
              +1

              Извините, я не ставил вам минус, я всего лиш ответил на ваш комментарий.

              Is AngularJS a library, framework, plugin or a browser extension?
              AngularJS fits the definition of a framework the best, even though it's much more lightweight than a typical framework and that's why many confuse it with a library.

              AngularJS is 100% JavaScript, 100% client-side and compatible with both desktop and mobile browsers. So it's definitely not a plugin or some other native browser extension.


    1. alek0585
      18.05.2016 12:27
      +2

      Мне кажется или ваш питон и джаваскрипт код не делают одного и того же? У питона не хватает обработки «неудачи»(catch на js), непонятная лишняя скобка во второй строке, дважды вызывается x.adata. Мне было тяжело читать этот код… Вот оцените другой вариант

      let doMe = (x, y, z) => {
          return x.atata(y, z).then((k) => { 
              console.log(k);
          }
      }
      
      


      async def doMe(x, y, z):
          try:
              k = await x.atata(y, z)
              print(k)
          except:
              pass
          
      


      Спасибо


      1. Tirael78
        18.05.2016 13:39

        ну уж если вы насчет правильности и чтения кода то в python перехватывать все исключения используя голый except — не правильно, вы получите и системные ошибки, нужно хотя бы так
        except Exception as exc:

        ну и во вторых использовать pass при обработке исключений — плохой тон.


  1. bosha
    18.05.2016 12:59
    +1

                for r in ['/login', '/static/', '/signin', '/signout', '/_debugtoolbar/']:
                    if path.startswith(r):
                        result = False
                return result
    


    Ох…


    1. Crandel
      18.05.2016 13:16

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


      1. bosha
        18.05.2016 15:30

        Ну, главное чтобы народ бездумно не копировал такие примеры. :)


  1. roller
    23.05.2016 00:43

    Эта штука чем то отличается принципиально от рубевой EventMachine?


    1. Crandel
      23.05.2016 07:11

      Никогда не писал на руби и не знаю, что такое EventMachine