что такое асинхронный запрос на отправку письма
Создаем массу асинхронных запросов при помощи Grequests
Requests хорошо, но grequests лучше. Я не знаю лучше, эффективней библиотеку, которая умеет быстро и элегантно выполнять HTTP-запросы нежели requests, данная библиотека — несомненный лидер, в данном плане.
Но так как с асинхронностью, у неё хромает, выполнять асинхронные запросы возможно с использованием threading или gevent.
Написал grequests, тот же автор что и написал requests. Только с использованием gevent + requests. Не буду долго мусолить тему, дам вам подробную информацию о данной библиотеки.
Grequests — является асинхронной обёрткой над обычной requests.
Сделаем обычный POST-запрос на множество url-адресов:
Все довольно просто, импортируется библиотека, открывается файл на чтение, создается список, переменной params присваивается значения a:b, c:d.
Далее создаем переменную rs которая будет отвечать за сам POST-запрос, для переменной r создаем grequests.map([rs], size=является асинхронным значением, чем больше значение тем быстрее будут выполнятся http-запросы, правда больше 16 не имеет смысла ставить).
Теперь так как мы передали все аргументы в переменную r, то есть в grequests.imap() мы можем взаимодействовать с данной переменной как в обычном requests.
И последним шагом нам нужно вывести все status code, url address, также rs выступает списком, это мы делаем для того чтоб не было ошибок индексации по типу:
TypeError: ‘Response’ object does not support indexing
Если у вас все ровно вылезает traceback с данной ошибкой, предлагаю вариант:
Теперь и к переменной r мы будем обращаться как к списку, дабы избежать ошибок индексации.
Основные шаги, мы сделали. Можете «ДУДОСИТЬ» сервера. Хотя, заоблачной асинхронностью данная библиотека не обладает. Это можно приглянуть к Aiohttp.
Ещё хотел бы поговорить о исключениях в grequests. Так как grequests не использует error-классы из requests, а делает следующим образом:
Мы ловим при помощи exception_handler:
Полный исходный код:
Так мы сможем с полной уверенностью отлавливать ошибки.
С GET-запросами все так-же просто как и с POST-запросами:
Асинхронные HTTP-запросы на C++: входящие через RESTinio, исходящие через libcurl. Часть 1
Наша команда занимается разработкой небольшого, удобного в использовании, встраиваемого, асинхронного HTTP-сервера для современного C++ под названием RESTinio. Начали его делать потому, что нужна была именно асинхронная обработка входящих HTTP-запросов, а ничего готового, чтобы нам понравилось, не нашлось. Как показывает жизнь, асинхронная обработка HTTP-запросов в C++ приложениях нужна не только нам. Давеча на связь вышли разработчики из одной компании с вопросом о том, можно ли как-то подружить асинхронную обработку входящих запросов в RESTinio с выдачей асинхронных исходящих запросов посредством libcurl.
По мере выяснения ситуации мы обнаружили, что эта компания столкнулась с условиями, с которыми сталкивались и мы сами, и из-за которых мы и занялись разработкой RESTinio. Суть в том, что написанное на C++ приложение принимает входящий HTTP-запрос. В процессе обработки запроса приложению нужно обратиться к стороннему серверу. Этот сервер может отвечать довольно долго. Скажем, 10 секунд (хотя 10 секунд — это еще хорошо). Если делать синхронный запрос к стороннему серверу, то блокируется рабочая нить, на которой выполняется HTTP-запрос. А это начинает ограничивать количество параллельных запросов, которые может обслуживать приложение.
Выход в том, чтобы приложение могло асинхронно обрабатывать все запросы: и входящие, и исходящие. Тогда на ограниченном пуле рабочих нитей (а то и вообще на одной единственной рабочей нити) можно будет обрабатывать одновременно десятки тысяч запросов, пусть даже время обработки одного запроса исчисляется десятками секунд.
Фокус был в том, что в приложении для исходящих HTTP-запросов уже использовался libcurl. Но в виде curl_easy, т.е. все запросы выполнялись синхронно. У нас же спрашивали, а можно ли совместить RESTinio и curl_multi? Вопрос для нас самих оказался интересным, т.к. раньше libcurl в виде curl_multi применять не приходилось. Поэтому интересно было самим погрузиться в эту тему.
Погрузились. Получили массу впечатлений. Решили поделиться с читателями. Может кому-нибудь будет интересно, как можно жить с curl_multi. Ибо, как показала практика, жить-то можно. Но осторожно… 😉 О чем мы и расскажем в небольшой серии статей, основанных на опыте реализации несложной имитации описанной выше ситуации с медленно отвечающим сторонним сервисом.
Необходимые disclaimer-ы
Дабы предупредить бесполезный и неконструктивный флейм в комментариях (вроде того, что случилось с предыдущей статьей), хочется сделать несколько предупреждений:
В чем суть разработанной имитации?
В демонстрационных целях мы с помощью RESTinio и libcurl сделали несколько приложений. Самое простое из них — это имитатор стороннего, медленно отвечающего сервера, под названием delay_server. Для запуска имитации нужно запустить delay_server с необходимым набором параметров (адрес, порт, желаемые времена задержек для ответов).
Так же в имитацию входит несколько «фронтов», под названием bridge_server_*. Именно bridge_server-а принимают запросы от пользователя и переадресуют запросы на delay_server. Предполагается, что пользователь запускает сперва delay_server, потом один из bridge_server-ов, после чего уже начинает «обстреливать» bridge_server удобным ему способом. Например, через curl/wget или утилиты вроде ab/wrk.
В состав имитации входит три реализации bridge_server-ов:
delay_server
Что делает delay_server?
delay_server принимает HTTP GET запросы на URL-ы вида /YYYY/MM/DD, где YYYY, MM и DD — это цифровые значения. На все остальные запросы delay_server отвечает кодом 404.
Если же приходит HTTP GET запрос на URL вида /YYYY/MM/DD, то delay_server выдерживает паузу и затем отвечает небольшим текстом, в котором есть приветствие «Hello, World» и величина выдержанной паузы. Например, если запустить delay_server с параметрами:
т.е. он будет слушать на localhost:4040 и выдерживать паузу для ответов между 1.5s и 4.0s. Если затем выполнить:
Ну или можно включить трассировку происходящего. Для сервера это:
Для delay_server-а мы увидим что-то вроде:
Как delay_server это делает?
delay_server представляет из себя простое однопоточное C++ приложение. На главной нити запускается встроенный HTTP-сервер, который дергает назначенный пользователем callback при получении запроса на подходящий URL. Этот callback создает Asio-шный таймер и взводит созданный таймер на случайно выбранную паузу (пауза выбирается так, чтобы попасть в заданные при запуске delay_server пределы). После чего callback возвращает управление HTTP-серверу, что дает возможность серверу принять и обработать следующий запрос. Когда срабатывает взведенный callback-ом таймер, то формируется и отсылается ответ на ранее полученный HTTP-запрос.
Разбор реализации delay_server
Функция main()
Разбор реализации delay_server начнем сразу с функции main(), постепенно объясняя то, что происходит внутри и вне main()-а.
Итак, код main() выглядит следующим образом:
Что здесь происходит?
Во-первых, мы разбираем аргументы командной строки и получаем объект с конфигурацией для delay_server-а.
Во-вторых, мы создаем несколько объектов, которые нам понадобятся:
Вот, собственно и весь delay_server 🙂
Но дьявол, как водится, в деталях. Поэтому пойдем дальше, рассмотрим что же прячется за этими простыми действиями.
Конфигурация и разбор командной строки
В delay_server используется очень простая структура для описания конфигурации сервера:
Разбор командной строки довольно таки объемный, поэтому погружаться в него особо не будем. Но желающие могут заглянуть под спойлер, чтобы составить впечатление о происходящем.
Для разбора мы попробовали использовать новую библиотеку Clara от автора широко известной в узких кругах библиотеки для unit-тестов в C++ под названием Catch2 (в девичестве просто Catch).
В общем-то здесь ничего сложного за исключением одного фокуса: функция parse_cmd_line_args возвращает экземпляр локально определенной структуры. По хорошему, здесь следовало бы возвращать что-то вроде:
Но в C++14 std::variant нет, а тащить какую-то реализацию variant/either из сторонней библиотеки или же полагаться на наличие std::experimental::variant не хотелось. Поэтому сделали вот так. Код, конечно, попахивает, но для слепленной на коленке имитации пойдет.
Генератор случайных задержек
Тут вообще все просто, обсуждать, в принципе, нечего. Поэтому просто код. Ради того, чтобы был.
Функция handler()
Один из ключевых элементов реализации delay_server — это небольшая функция handler(), внутри которой и происходит обработка входящих HTTP-запросов. Вот весь код этой функции:
Эта функция (посредством лямбды, созданной в main()-е) вызывается каждый раз, как HTTP-сервер принимает входящий GET-запрос на нужный URL. Сам входящий HTTP-запрос передается в параметре req типа restinio::request_handle_t.
Этот самый restinio::request_handle_t представляет из себя умный указатель на объект с содержимым HTTP-запроса. Что позволяет сохранить значение req и воспользоваться им позже. Именно это и является одним из краеугольных камней в асинхронности RESTinio: RESTinio дергает предоставленный пользователем callback и передает в этот callback экземпляр request_handle_t. Пользователь может либо сразу сформировать HTTP-ответ внутри callback-а (и тогда это будет тривиальная синхронная обработка), либо же может сохранить req у себя или передать req какой-то другой нити. После чего вернуть управление RESTinio. И сформировать ответ позже, когда для этого наступит подходящее время.
В данном случае создается экземпляр asio::steady_timer и req сохраняется в лямбда-функции, передаваемой в async_wait для таймера. Соответственно, объект HTTP-запроса сохраняется до тех пор, пока не сработает таймер.
Очень важный момент в handler()-е — это возвращаемое им значение. По возвращаемому значению RESTinio понимает взял ли пользователь ответственность за формирование ответа на запрос или нет. В данном случае возвращается значение request_accepted, что означает, что пользователь пообещал RESTinio сформировать ответ на входящий HTTP-запрос позже.
А вот если бы handler() возвратил, скажем, request_rejected(), то RESTinio бы закончил обработку запроса и ответил бы пользователю кодом 501.
Итак, handler() вызывается когда приходит входящий HTTP-запрос на нужный URL (почему именно так рассматривается ниже). В handler-е вычисляется величина задержки для ответа. После чего создается и взводится таймер. Когда таймер сработает, будет сформирован ответ на запрос. Ну и handler() обещает RESTinio сформировать ответ на запрос путем возврата request_accepted.
Вот, собственно, и все. Маленькая мелочь: для формирования тела ответа используется fmtlib. В принципе, здесь без нее можно было бы и обойтись. Но, во-первых, нам fmtlib очень нравится и мы используем fmtlib при удобном случае. И, во-вторых, нам fmtlib все равно потребовалась в bridge_server-ах, так что не было смысла отказываться от нее в delay_server.
Функция run_server()
Функция run_server() отвечает за настройку и запуск HTTP-сервера. Она определяет какие запросы HTTP-сервер будет обрабатывать и как HTTP-сервер будет отвечать на все остальные запросы.
Так же в run_server() определяется где будет работать HTTP-сервер. Для случая delay_server это будет главная нить приложения.
Давайте сперва посмотрим на код run_server(), а потом рассмотрим несколько важных моментов, о которых мы еще не говорили.
Что в ней происходит и почему это происходит именно так?
Во-первых, для delay_server будет использоваться функциональность, аналогичная системе роутинга запросов expressjs. В RESTinio это называется Express router.
Нужно создать экземпляр объекта, который отвечает за маршрутизацию запросов на основе регулярных выражений. После чего в этот объект нужно поместить список маршрутов и задать каждому маршруту свой обработчик. Что мы и делаем. Создаем обработчик:
И указываем интересующий нас маршрут:
После чего еще и задаем обработчик для всех остальных запросов. Который просто будет отвечать кодом 404:
На этом подготовка нужного нам Express router-а завершается.
Во-вторых, при вызове run() мы указываем, что HTTP-сервер должен использовать заданный io_context и должен работать на той самой нити, на которой и сделали вызов run(). Плюс к тому для сервера задаются параметры из конфигурации (т.к. IP-адрес и порт, максимально допустимое время для обработки запросов и сам обработчик):
Здесь использование on_this_thread как раз и заставляет RESTinio запустить HTTP-сервер на контексте той же самой нити.
Почему run_server() — это шаблон?
Функция run_server() является функцией-шаблоном, зависящей от двух параметров:
Для того, чтобы пояснить, почему это так, начнем со второго шаблонного параметра — Handle.
Внутри main() мы создаем актуальный обработчик запросов в виде лямбда-функции. Реальный тип этой лямбды знает только компилятор. Поэтому для того, чтобы передать лямбду-обработчик в run_server() нам и нужен шаблонный параметр Handle. С его помощью компилятор сам выведет нужный тип аргумента handler в run_server().
А вот с параметром Server_Traits ситуация чуть посложнее. Дело в том, что HTTP-серверу в RESTinio нужно задать набор свойств, которые будут определять различные аспекты поведения и реализации сервера. Например, будет ли сервер приспособлен к работе в многопоточном режиме. Будет ли сервер выполнять логирование выполняемых им операций и т.д. Все это задается шаблонным параметром Traits для класса restinio::http_server_t. В данном примере этого класса не видно, т.к. экземпляр http_server_t создается внутри run(). Но все равно Traits должны быть заданы. Как раз шаблонный параметр Server_Traits функции run_server() и задает Traits для http_server_t.
Нам в delay_server потребовалось определить два разных типа Traits:
Первый тип, non_traceable_server_traits_t, используется когда сервер не должен логировать свои действия. Второй тип, traceable_server_traits_t, используется когда логирование должно быть.
Соответственно, внутри функции main(), в зависимости от наличия или отсутствия ключа «-t», функция run_server() вызывается либо с non_traceable_server_traits_t, либо с traceable_server_traits_t:
Так что назначение нужных свойств HTTP-серверу — это еще одна причина того, почему run_server() является функцией-шаблоном.
Более детально тема Traits для restinio::http_server_t затронута в нашей предыдущей статье о RESTinio.
Заключение первой части
Вот, собственно, и все, что можно было рассказать о реализации delay_server-а на базе RESTinio. Надеемся, что описанный материал оказался понятен. Если нет, то с удовольствием ответим на вопросы в комментариях.
В последующих статьях мы уже будем говорить о примерах интеграции RESTinio и curl_multi, разбирая реализации bridge_server_1 и bridge_server_2. Там части, которые относятся именно к RESTinio, будут не объемнее и не сложнее того, что мы показали в этой статье. А основной объем кода и основная сложность будет проистекать из-за curl_multi. Но это уже совсем другая история…
Мега-Учебник Flask, Часть X: Поддержка электронной почты (издание 2018)
Miguel Grinberg
Туда Сюда
Это десятая часть серии Mask-Tutorial Flask, в которой я расскажу вам, как приложение может отправлять электронные письма вашим пользователям и как создать функцию восстановления пароля при поддержке адреса электронной почты.
Под спойлером приведен список статей этой серии.
Примечание 1: Если вы ищете старые версии данного курса, это здесь.
Примечание 2: Если вдруг Вы захотели бы выступить в поддержку моей(Мигеля) работы, или просто не имеете терпения дожидаться статьи неделю, я (Мигель Гринберг)предлагаю полную версию данного руководства(на английском языке) в виде электронной книги или видео. Для получения более подробной информации посетите learn.miguelgrinberg.com.
В итоге, после не долгих терзаний, по прошествии 9-ти уроков мы получили приложение, которое сносно работает с базой данных, поэтому в этой главе я хочу уйти от этой темы и добавить еще одну важную часть, которая нужна большинству веб-приложений, а именно — отправка электронной почты.
Почему приложение должно что то отправлять по электронной почте своим пользователям? Существует много причин, но одна из них — решение проблем, связанных с аутентификацией. В этой главе я собираюсь добавить функцию сброса пароля для пользователей, которые постоянно забывают свой пароль. Когда пользователь запрашивает сброс пароля, приложение отправляет электронное письмо со специально созданной ссылкой. Затем пользователю необходимо щелкнуть эту ссылку, чтобы получить доступ к форме, в которой можно установить новый пароль.
Введение в Flask-Mail
Что касается отправки электронной почты, то Flask имеет для этой цели расширение под названием Flask-Mail, которое поможет сделать эту задачу очень простой. Как всегда, оно (это расширение) устанавливается с помощью pip:
Ссылки на сброс пароля должны содержать в себе безопасный токен. Чтобы сгенерировать эти токены, я собираюсь использовать JSON Web Tokens, который также имеет популярный пакет для Python:
Как и для большинства расширений Flask, вам нужно создать экземпляр сразу после создания приложения Flask. В этом случае это объект класса Mail :
Для того, что бы протестировать отправку электронных писем, у вас есть те же два варианта, о которых я упоминал в главе 7. Если вы хотите использовать эмулированный почтовый сервер, то Python предоставляет вариант для запуска во втором терминале с помощью следующей команды:
Чтобы настроить этот сервер, необходимо установить две переменные среды:
Помните, что параметры безопасности вашей учетной записи Gmail могут препятствовать приложению отправлять электронную почту через нее, если вы явно не разрешаете «менее безопасным приложениям» доступ к вашей учетной записи Gmail. Вы можете прочитать об этом здесь, и если вас беспокоит безопасность вашей учетной записи, вы можете создать вторичную, которую вы настраиваете только для проверки электронных писем, или вы можете временно включить параметр разрешить доступ «менее безопасные приложениям» для запуска своих тестов, а затем вернуться назад к более безопасному по умолчанию.
Использование Flask-Mail
Для демонстрации работы Flask-Mail, я покажу вам, как отправить электронное письмо из оболочки Python. Для этого запустите Python с flask shell, а затем выполните следующие команды:
Короче, это довольно просто. Теперь давайте интегрируем электронные письма в приложение.
Простой Email Framework
Начнем с написания вспомогательной функции, отправляющей электронное письмо. В общих чертах она повторяет собой упражнение flask shell из предыдущего раздела. Я положу эту функцию в новый модуль под названием app/email.py :
Запрос сброса пароля
Напомню что, задача которую мы решаем, заключается в предоставлении пользователю возможности сбросить свой пароль. Для этого я собираюсь добавить ссылку на страницу входа:
Когда пользователь нажмет на ссылку Click to Reset It, появится новая веб-форма, которая запрашивает адрес электронной почты пользователя в качестве способа инициирования процесса сброса пароля. Вот класс формы:
И вот соответствующий HTML-шаблон:
Понадобится также view-функция для обработки этой формы:
Это функция просмотра сильно смахивает на другие, которые обрабатывают форму. Стартуем с того, что пользователь не вошел в систему. Если пользователь вошел в систему, то нет смысла использовать функцию сброса пароля, а следует перенаправить вывод на страницу index.
После отправки сообщения электронной почты я вывел сообщение, предлагающее пользователю проверить электронную почту в своем ящике, где он должен обнаружить послание с инструкциями и перенаправлением обратно на страницу входа в систему. Вы можете заметить, что это сообщение отображается в любом случае. Это значит, что клиенты не могут использовать эту форму, чтобы выяснить, зарегестрирован данный пользователь или нет.
Токены сброса пароля
Ссылки будут снабжены токеном, и этот токен будет проверен, прежде чем разрешить изменение пароля, в качестве доказательства того, что пользователь, который запросил электронное письмо, имеет доступ к адресу электронной почты в учетной записи. Очень популярным стандартом токена для этого типа процесса является JSON Web Token, или JWT. Самое приятное в JWT заключается в том, что они самодостаточны. Вы можете отправить токен пользователю по электронной почте, и когда пользователь нажмет на ссылку, которая возвращает токен обратно в приложение, его можно проверить самостоятельно.
Что бы разобраться как работают JWT? Ничего лучше не придумать, как испытать это в сеансе оболочки Python:
Как видите, итоговый токен — это длинная последовательность символов. Но не думайте, что это зашифрованный токен. Содержимое токена, включая полезную нагрузку, может быть легко декодировано любым пользователем (не верите мне? Скопируйте вышеуказанный токен, а затем введите его в отладчик JWT, чтобы просмотреть его содержимое). Что делает маркер безопасным, так это то, что полезная нагрузка имеет подпись. Если кто-то пытался подделать или манипулировать полезной нагрузкой в токене, тогда подпись будет признана недействительной, а для создания новой подписи необходим секретный ключ. Когда токен проверен, содержимое полезной нагрузки декодируется и возвращается обратно вызывающему абоненту. Если подпись токена была подтверждена, то полезной нагрузке можно доверять как аутентичной.
Полезная нагрузка, которую я буду использовать для токенов сброса пароля, будет иметь формат <'reset_password': user_id, 'exp': token_expiration>. Поле exp является стандартным для JWT, и если оно присутствует, то это указывает на время истечения срока действия токена. Если у токена есть действительная подпись, но она превысила отметку времени истечения срока действия, то такая подпись будет считаться недействительной. Для функции сброса пароля я собираюсь дать этим токенам 10 минут жизни.
Когда пользователь нажимает на ссылку в письме полученном по электронной почте, токен будет отправлен обратно в приложение как часть URL-адреса, и первым делом функция просмотра, обрабатывающая этот URL-адрес, будет проверять его. Если подпись действительна, то пользователь может быть идентифицирован идентификатором, хранящимся в полезной нагрузке. Далее, как только идентификация пользователя пройдет проверку, приложение может запросить новый пароль и установить его в учетной записи пользователя.
Поскольку эти токены принадлежат пользователям, я собираюсь написать функции генерации и проверки токена как методы в модели User :
Функция get_reset_password_token() генерирует токен JWT в виде строки. Обратите внимание, что decode(‘utf-8’) необходим, потому что функция jwt.encode() возвращает токен в виде последовательности байтов, но в приложении удобнее иметь токен в виде строки.
Отправка электронной почты для сброса пароля
Или вот так, поприличней, в HTML-версии почти того же письма с более приличным текстом:
Сброс пароля пользователя
Когда пользователь нажимает на ссылку электронной почты, сработает второй маршрут, связанный с этой функцией. Вот функция просмотра запроса пароля:
Вот класс ResetPasswordForm :
А это соответствующий шаблон HTML:
Вот сейчас функция сброса пароля завершена. Давайте, попробуйте.
Асинхронные сообщения
Если вы используете имитацию сервера электронной почты, который предоставляет Python, возможно, вы этого не заметили, но отправка электронной почты значительно замедляет приложение. Все взаимодействия, которые должны произойти при отправке электронной почты, делают задачу медленной, обычно требуется несколько секунд, чтобы получить электронную почту, и, возможно, больше, если сервер электронной почты адресата работает медленно или если есть несколько адресатов.
Хотелось бы, чтобы функция send_email() была асинхронной. Что это значит? Это означает, что при вызове этой функции задача отправки электронной почты запланирована в фоновом режиме, освобождая send_email() для немедленного возврата, чтобы приложение могло продолжать работать одновременно с отправляемым электронным письмом.
У Python есть поддержка для запуска асинхронных задач, фактически более чем одним способом. Могут выполняться поточные, и многопроцессорные ( threading и multiprocessing ) модули. Запуск фонового потока для отправленного сообщения намного менее ресурсоемкий, чем запуск совершенно нового процесса, поэтому я собираюсь пойти именно таким путем:
Туда Сюда