Как написать свой python веб-фреймворк
Каждый из нас хотя бы раз в жизни мечтал сделать что-то "как это, но лучше". Сегодня мы частично поддадимся этому желанию и, закрыв глаза на стоящую за ним глупость, рассмотрим с чего начать создание своего 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), который будет принимать запросы и выполнять наш код:
uvicorn main:app
Переходим в браузер, вводим адрес из лога uvicorn, смотрим — оно живое!
Вы и не заметили, но к данному абзацу у нас есть всё необходимое, чтобы начать имплементацию своего собственного фреймворка, ведь тот же FastAPI, который я так люблю упоминать, работает точно так же! Роутинг, промежуточные слои, сессии и прочее — всё это условности и расширения функций, которые реализуют сами фреймворки поверх стандартов WSGI и ASGI.
Есть небольшая уловка, которая избавляет разработчиков FastAPI от работы с "низкоуровневым" ASGI кодом. Уловка эта называется starlette и является по сути обёрткой над ASGI, которая делает всю грязную работу, но это мы рассмотрим (если рассмотрим) как-нибудь в другой раз.