13g10n
На главную

Как написать свой python веб-фреймворк

3 минуты

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

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

Здесь на сцену выходят такие понятия как WSGI и ASGI, которые являются стандартом взаимодействия между веб-сервером и python-приложением. Причем с точки зрения реализации ASGI — лишь следующий шаг в развитии протокола, поддерживающий асинхронность. Что на практике чаще всего сводится к поддержке вебсокетов и увеличению в разы количества обрабатываемых запросов в секунду за счёт использования времени простоя при IO операциях, но сейчас не об этом.

Сам стандарт, как я и обещал, чертовски прост — нужно всего-то реализовать одну функцию, которая будет принимать два параметра: информацию об окружении и обработчик.

def wsgi_app(environ, start_response):
    ...

Далее, всё что от нас требуется — вызвать обработчик start_response передав код ответа и заголовки, а после вернуть итератор с телом ответа. Можно использовать yield, чтобы превратить функцию в генератор, а можно просто использовать return iter(...) — тут уже ваша реализация.

Более интересным и живым является, конечно же, ASGI. На нём мы и остановимся, чтобы рассмотреть немного подробнее, т.к. писать в 2022 году фреймворк под WSGI как минимум глупо.

async def app(scope, receive, send):
    await send(...)

Отличия небольшие, но какие! Во-первых, наша функция стала async, что уже позволяет нам использовать разного рода преимущества асинхронного программирования и увеличить производительность. Во-вторых, мы получили немного иные параметры: scope, который содержит информацию о запросе (тип, метод, путь, заголовки и т.д.) и две корутины receive и send (они же асинхронные функции, которые мы должны подвергнуть жесточайшему await'у) для получения и отправки данных.

Теперь, зная теорию, мы вполне можем написать своё первое ASGI приложение, которое будет отвечать на HTTP запросы, возвращая простой текст "It's alive!" в ответ:

async def app(scope, receive, send):
    assert scope['type'] == 'http'

    await send({
        'type': 'http.response.start',
        'status': 200,
        'headers': [
            [b'content-type', b'text/plain'],
        ],
    })
    await send({
        'type': 'http.response.body',
        'body': b'It\'s alive!',
    })

Ну и запустим какой-нибудь ASGI сервер (например, uvicorn), который будет принимать запросы и выполнять наш код:

Terminal
uvicorn main:app

Переходим в браузер, вводим адрес из лога uvicorn, смотрим — оно живое!

Вы и не заметили, но к данному абзацу у нас есть всё необходимое, чтобы начать имплементацию своего собственного фреймворка, ведь тот же FastAPI, который я так люблю упоминать, работает точно так же! Роутинг, промежуточные слои, сессии и прочее — всё это условности и расширения функций, которые реализуют сами фреймворки поверх стандартов WSGI и ASGI.

Есть небольшая уловка, которая избавляет разработчиков FastAPI от работы с "низкоуровневым" ASGI кодом. Уловка эта называется starlette и является по сути обёрткой над ASGI, которая делает всю грязную работу, но это мы рассмотрим (если рассмотрим) как-нибудь в другой раз.

PythonWSGIASGI