В поисках работы, которая затягивает?

Путь от выполнения запроса в браузере до Python-приложения

  • 2 ч.

Путь от выполнения запроса в браузере до Python-приложения



Веб-приложение или веб-сайт вращаются вокруг цикла запрос-ответ согласно протоколу HTTP, и Python-приложения не являются исключением. Но это не просто двухэтапный процесс. Наши приложения должны пройти различные стадии, чтобы вернуть конечному пользователю результат. Чтобы лучше понять структуру Django либо Flask, мы должны понимать, как инициируются запросы и как конечный результат передается конечному пользователю. Свое начало наш запрос берет еще в адресной строке браузера, после чего происходит ряд действий, которые приводят его непосредственно к Python-приложению.




Коротко о всех стадиях

  • DNS: сопоставление доменного имени с соответствующим ему IP-адресом
  • Подключение по IP адресу к веб-серверу - Nginx – Передача данных HTTP-запроса от веб-сервера к серверу приложений - uWSGI посредством unix-сокета
  • uWSGI извлекает данные запроса из unix-сокета и выполняет:
    • активацию виртуального окружения
    • запуск интерпретатора Python
    • вызов точки входа приложения
    • возврат итератора, из которого формируется тело запроса
  • Разрешение URL внутри приложения



DNS

Компьютеры находят друг друга в интернете по IP-адресам. Чтобы подключиться к серверу с конкретным сайтом, нужно знать его IP-адрес. Похожим образом устроена мобильная связь: чтобы позвонить конкретному человеку, нужно знать его номер.

Мы сохраняем важные номера в контакты смартфона. Только в случае с доменами, ничего сохранять не нужно. Мы просто вводим в адресной строке домен, а браузер сам находит IP-адрес нужного сервера и открывает сайт.

Так что же представляет собой DNS?

DNS (Domain Name System «система доменных имён») — компьютерная распределённая система для получения информации о доменах.

Настройки каждого домена в интернете хранятся в текстовых файлах на DNS-серверах. Адреса этих серверов обычно приходится указывать вручную — их присылает хостинг-провайдер. Например, у нас они выглядят так: dns1.ask42.us и dns2.ask42.us.

Браузеры используют DNS-сервер вашего провайдера, чтобы узнать IP-адрес сервера, на котором находится сайт. Для этого в каждом браузере есть специальная программа — DNS-клиент.

DNS-клиент

DNS-клиент будет посылать запрос по дереву иерархии DNS-серверов до тех пор, пока один из них не укажет или не приведет к необходимому IP-адресу.




Веб-сервер - Nginx

Nginx - веб-сервер, поддерживающий большое количество соединений одновременно, обеспечивающий их безопасность и надежность. В своём первоначальном выпуске он функционировал для веб-обслуживания HTTP. Однако сегодня Nginx также служит обратным прокси-сервером, балансировщиком нагрузки HTTP и почтовым прокси-сервером для IMAP, POP3 и SMTP.

Итак, получив запрос после разрешения DNS-имени, веб-сервер передает его серверу приложений

  • согласно IP-адресу и TCP-порту соединения (порт протокола http - 80, https - 443 и др.)
  • при помощи UNIX-сокета

В данной статье мы остановимся на более распространённом и уместном варианте, а именно общение с помощью UNIX-сокета, который обеспечивает более высокую производительность, чем связка TCP/IP.

Подробнее о Unix-сокетах

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

В отличии от именованных каналов(портов), при использовании сокетов прослеживается отличие между клиентом и сервером. Механизм сокетов позволяет создавать сервер к которому подключается множество клиентов.

Со стороны сервера:

  • системный вызов socket создаёт сокет, но этот сокет не может использоваться совместно с другими процессами
  • сокет именуется. Для unix-сокета адрес будет задан именем файла
  • системный вызов listen(int socket, int backlog) формирует очередь входящих подключений. Второй параметр backlog определяет длину этой очереди
  • эти подключения сервер принимает с помощью вызова accept, который создаёт новый сокет, отличающийся от именованного сокета. Этот новый сокет применяется только для взаимодействия с данным конкретным клиентом.

Со стороны клиента подключение происходит несколько проще:

  • вызывается socket
  • и connect, используя в качестве адреса именованный сокет сервера.

Таким образом, веб-сервер Nginx передает данные запроса(трафик) серверу приложений uWSGI при помощи unix-сокета.




Сервер приложений - uWSGI

Веб-сервер может по запросу отдавать пользователям файлы из своей файловой системы, однако он не может напрямую работать с Python приложениями. Веб-серверу нужен интерфейс, который будет запускать приложение, передавать ему запрос от пользователя и возвращать ответ.

Для выполнения этих задач был разработан Web Server Gateway Interface — WSGI — стандарт взаимодействия Python программ и веб-сервера.

uWSGI - сервер приложений, процессы которого происходят согласно интерфейсу, определенного WSGI.

На данном сервере активируется виртуальное окружение(venv), с установленными пакетами Python, Django/Flask и т.д.

Далее запускается интерпретатор Python, после чего, получив сокет от Nginx, uWSGI в свою очередь:

  • Вызовет точку входа, полученную с кодом состояния HTTP и заголовками
  • Будет возвращать итератор, при помощи которого будет сгенерировано тело запроса клиента

Файл конфигурации WSGI, скорее всего, будет использоваться как ссылка на остальной код приложения. к примеру, Django-проекты включают файл wsgi.py по умолчанию.




Подробнее о виртуальном окружении - virtual environment

При разработке Python-приложений или использовании решений на Python, созданных другими разработчиками, может возникнуть ряд проблем, связанных с использованием библиотек различных версий. Рассмотрим их более подробно.

Во-первых: различные приложения могут использовать одну и туже библиотеку, но при этом требуемые версии могут отличаться.

Во-вторых: может возникнуть необходимость в том, чтобы запретить вносить изменения в приложение на уровне библиотек, т.е. вы установили приложение и хотите, чтобы оно работало независимо от того обновляются у вас библиотеки или нет. Как вы понимаете, если оно будет использовать библиотеки из глобального хранилища (/usr/lib/pythonXX/site-packages), то, со временем, могут возникнуть проблемы.

В-третьих: у вас просто может не быть доступа к каталогу /usr/lib/pythonXX/site-packages.

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


Краткое описание ПО для работы с виртуальными окружениями Python:

  • virtualenv - один из самых популярных инструментов, позволяющих создавать виртуальные окружения. Он прост в установке и использовании.
  • venv - этот модуль появился в Python3 и не может быть использован для решения задачи изоляции в Python2. По своему функционалу очень похож на virtualenv.
  • pyenv - Инструмент для изоляции версий Python. Чаще всего применяется, когда на одной машине вам нужно иметь несколько версий интерпретатора для тестирования на них разрабатываемого вами ПО.
  • Virtualenvwrapper – это обертка для virtualenv позволяющая хранить все изолированные окружения в одном месте, создавать их, копировать и удалять.


Как работает виртуальная среда?

Что именно имеется ввиду под «активировать» среду? Понимание того, что именно происходит под капотом, может быть очень важно для разработчика, особенно когда вам нужно понять выполнение виртуальной среды, разрешение зависимостей, и так далее.

Чтобы объяснить, как это работает, для начала проверим расположения разных исполняемых файлов python. С «деактивированной» средой запускаем:

>>>
		$ which python
		/usr/bin/python
		

Теперь активируем и снова запустим команду:

>>>
		$ source env/bin/activate
		(env) $ which python
		/Users/ask42user/python-virtual-environments/env/bin/python
		

Активировав среду, мы теперь получаем другой путь к исполняемому файлу python, так как в активной среде переменная среды $PATH несколько отличается.

Обратите внимание на разницу между первым путем в $PATH до и после активации:

>>>
		$ echo $PATH
		/usr/local/bin:/usr/bin:/bin:/usr/sbin:/sbin:

		$ source env/bin/activate
		(env) $ echo $PATH
		/Users/ask42user/python-virtual-environments/env/bin:/usr/local/bin:/usr/bin:/bin:/usr/sbin:/sbin:
		

В последнем примере, каталог bin нашей виртуальной среды теперь находится в начале пути. Это значит, что это первый каталог в поиске, когда мы запускаем исполняемый файл в командной строке. Таким образом, оболочка использует экземпляр нашей виртуальной среды в Python, а не в системной версии.


Возникают вопросы:

  • В чем разница между этими исполняемыми файлами?
  • Каким образом виртуальная среда исполняемого файла Python может использовать что-либо, кроме системных сайт-пакетов?

Это можно объяснить тем, как Python запускается и где он расположен в системе. Нет разницы между двумя исполняемыми файлами Python. Суть заключается в расположении каталога.

Когда Python запускается, он ищет путь своего двоичного файла (в виртуальной среде он является копией или символической ссылке системного бинарного файла Python). Далее, он устанавливает расположение sys.prefix и sys.exec_prefix согласно с этим расположением, опуская часть bin в пути.

Путь, находящийся в sys.prefix далее используется для поиска каталога site-packages, путем поиска по связанного с ним пути lib/pythonX.X/site-packages/, где Х.Х – это версия используемого вами Python.

В нашем примере, бинарный файл расположен в

	 /Users/ask42user/python-virtual-environments/env/bin
		

это значит, что sys.prefix может быть

	 /Users/ask42user/python-virtual-environments/env
		

следовательно, используемый каталог site-packages может быть

	 /Users/ask42user/python-virtual-environments/env/lib/pythonX.X/site-packages.
		

Наконец, этот путь наложен в массиве sys.path, который содержит все расположения, которые пакет может использовать.




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

У фреймворков Django и Flask имеются все необходимые инструменты для маршрутизации именно таких URL. Далее они будут рассмотрены подробнее.




Обработка запроса в Django

Прежде чем приступить к цепочке операций, производимых в Django, следует ознакомится с понятием Middleware.

Middleware (промежуточный слой) – это механизм «хуков» для обработки запросов и ответов.

Они предоставляют интерфейсы как приложению, так и серверу. То есть для сервера middleware является приложением, а для приложения сервером(согласно WSGI).

server-middleware-app


Промежуточное программное обеспечение используется в ряде ключевых функций в проекте Django: например мы используем промежуточное ПО CSRF для предотвращения атак подделки межсайтовых запросов.

Они используются для обработки данных сеанса. Аутентификация и авторизация осуществляется с использованием промежуточного программного обеспечения. Мы можем написать наши собственные классы промежуточного программного обеспечения для формирования (или замыкания) потока данных через наше приложение.

Существует четыре ключевых момента, которые вы можете подключить к циклу запрос/ответ через свое собственное промежуточное ПО:

  • process_request
  • process_response
  • process_view
  • process_exception

middleware_process


Все они в том или ином количестве реализованы у каждого стандартного middleware Django. Этапы, на которых они инициализируются и вызываются описаны ниже.




Итак, какие же процессы происходят далее?

В Django, получив сгенерированное тело запроса от uWSGI, тем же uWSGI вызывается точка входа - wsgi.py, представляющая из себя публичный интерфейс, который возвращает WSGI-callable - WSGIHandler(). Это обусловлено задачей скрыть его реализацию от точки входа на случай, если эта реализация изменится или будет перемещена в будущем.

WSGIHandler() в свою очередь:

  • Импортирует файл settings.py и классы исключений Django
  • Загружает все промежуточные классы, которые он находит внутри списка MIDDLEWARE, расположенном в settings.py
  • Создает четыре списка методов, которые обрабатывают запрос, представление, ответ и исключения(process_request, process_view, process_response, process_exception)
  • Перебирает методы запроса - process_request, выполняя их по порядку
  • Определяет запрошенный URL, предварительно получив его шаблон из подобного на рисунке запроса, переданного WSGI

    django_request

    • Файл settings.py будет иметь параметр ROOT_URLCONF, который указывает на корневой файл привязки urls.py, из которого будут включаться файлы urls.py для каждого из приложений (ROOT_URLCONF = 'mysite.urls' == mysite/urls.py)
    • Django просматривает файл привязки в поисках первого шаблона, который совпадёт с запрошенным URL. (список urlpatterns с элементами вызова функции path())
    • Если такой шаблон найден, Django импортирует и вызывает ассоциированную с ним функцию представления. (функция python из views.py)
  • Проходит через каждый из методов обработки представления - process_view
  • Вызывает функцию отображения (обычно рендеринг шаблона) с помощью контекстных менеджеров
  • Обрабатывает любые методы исключения - process_exception
  • Проходит через каждый из методов ответа (изнутри, в обратном порядке из промежуточного ПО запроса) - process_responce
  • Наконец, создает возвращаемое значение (response) и вызывает функцию обратного вызова на сервере приложения uWSGI для отправки ответа на веб-сервер и для пользователя.




Обработка запроса в Flask

После вызова точки входа обработчиком WSGI приложение получает объект тела запроса, имеющего вид следующий вид

flask_request

Внутри фреймворка происходит парсинг полученного объекта запроса, одной из целей которого является извлечение

  • метода запроса (GET, POST и т.д)
  • тела запроса либо шаблона (например '/', '/hello')

В итоге, после получения шаблона запроса используется декоратор route() для связывания функций с URL. Вот несколько основных примеров:

@app.route('/')
def index():
    return 'Index Page'
		
@app.route('/hello')
def hello():
    return 'Hello World'
		

Внутри Flask генерируется словарь для сопоставления url и функций обработчиков. В данном примере его ключами будут строки '/' и '/hello', a их соответственными значениями будет вызовы функций index() и hello(), внутри которых и будет генерироваться ответ(response) для сервера и пользователя.

Также стоит упомянуть, что Flask может генерировать URL с помощью функции url_for() из пакета flask. Функция url_for() принимает конечную точку и возвращает URL в виде строки.

К примеру, чтобы сгенерировать корневой URL, нужно вызвать url_for() следующим образом: url_for(‘index’). Выводом будет '/'.



Особенности URL при использовании Blueprint

Описанный выше случай оправдан в использовании лишь в относительно небольших и простый приложений Flask.

При разработке модульных приложений Flask использует концепцию blueprint’ов («blueprint» - «эскиз») для создания компонентов приложений и поддержки общих шаблонов внутри приложения или между приложениями. Blueprint’ы могут как значительно упростить большие приложения, так и предоставить общий механизм регистрации в приложении операций из расширений Flask.

Объект Blueprint работает аналогично объекту приложения Flask, но в действительности он не является приложением. Обычно это лишь эскиз для сборки или расширения приложения.

Основная концепция blueprint’ов заключается в том, что они записывают операции для выполнения при регистрации в приложении. Flask связывает функции представлений с blueprint’ами при обработке запросов и генерировании URL’ов от одной конечной точки к другой.

Касательно URL при использовании blueprint стоит отметить следующее:

  • При декорировании функций представления, связанных с blueprint, будет использоваться не имя приложения, а имя конкретного blueprint
    @app.route('/') -> @blueprint.route('/')
  • При использовании функции url_for() ее аргументу будет добавлен к конечной точке URL’а префикс с именем blueprint’а и точкой (.)
    url_for('admin.index') -> url_for('blueprint.index')




Таблица состояния запроса


Этап Состояние запроса
Browser ask42.us/request
DNS GET /request HTTP/1.1
NGINX HTTP/1.1 101 Switching Protocols
uWSGI [pid: 14|app: 0|req: 2/3] 172.18.0.1 () {42 vars in 849 bytes} [Mon Mar 2 08:06:21 2020] GET /request/ => generated 5478 bytes in 1432 msecs (HTTP/1.1 200) 5 headers in 167 bytes (1 switches on core 1)
Application method:GET url:/request