Как создать полностью асинхронный сервер приложений на Python
- Введение в асинхронное программирование
Асинхронное программирование может быть немного сложным со всем его жаргоном, но давайте разберем его простыми словами. Думайте об этом, как о выполнении множества дел по дому. Вы можете выполнять их по одному и дождаться завершения каждого, прежде чем начинать следующий (синхронно), или вы можете запустить один, а затем запустить другой, пока идет первый. Представьте, что вы начинаете стирку. Для вас не имеет смысла ждать, пока он закончится, прежде чем начинать готовить ужин. Гораздо эффективнее начать стирку в стиральной машине, а затем приготовить ужин, пока работает прачечная.
По сути, это то, что делает синтаксис async / await в библиотеке asyncio Python и в расширении tornado, которое негласно использует asyncio. Вы помечаете функцию как асинхронную (т. Е. Способную работать, пока ваша программа делает что-то еще), а затем ожидаете результата, пока программа продолжается. Все это выполняется в так называемом цикле ввода-вывода.
Цикл ввода-вывода запускает цикл, проверяя, перешла ли асинхронная функция из будущего (ожидающий объект) к завершенному результату. Думайте о будущем как о билетах из кафе. Это еще не настоящий кофе, а просто ожидаемый результат, когда бариста закончит его готовить. Все, что делает цикл ввода-вывода, - это постоянно проверять, какие фьючерсы ожидают и завершены. Вы создаете экземпляр объекта IO Loop один раз, и он работает как робот Рози, управляющий всеми задачами и проверяющий завершенные задачи.
2. Веб-платформа Tornado
Tornado - это веб-фреймворк, который пытается решить синхронную природу Python. Поскольку он использует asyncio, с помощью Tornado вы можете создать веб-сервер, подобный node.js, который работает асинхронно, что позволяет обрабатывать гораздо больше запросов, чем если бы он работал синхронно.
Для начала просто установите tornado с помощью pip-пакета или добавьте tornado в свой файл requirements.txt. Теперь давайте создадим удобный цикл ввода-вывода, который будет управлять всеми задачами и сервером, который будет прослушивать данный порт на предмет входящих запросов.
import tornado.ioloop from tornado.web import Application, RequestHandler class MainHandler(RequestHandler): async def get(self): data = await lots_of_data_func() self.write({"data":data}) def routes(): return Application([ (r"/", MainHandler), ]) if __name__ == "__main__": app = routes() app.listen(8888) tornado.ioloop.IOLoop.current().start()
В Tornado вы используете веб-обработчики на основе классов, поэтому мы создаем класс обработчика, а затем определяем запросы (получение, публикация, удаление и т. Д.) Как функции внутри класса. В приведенном выше примере у нас есть класс MainHandler с функцией запроса на получение. Убедитесь, что каждый класс наследуется от RequestHandler tornado.web, который содержит все необходимые компоненты для вашего класса для обработки входящих запросов.
Теперь о самом сервере. В нашем примере вы видите функцию routes (), которая возвращает объект приложения tornado.web, в котором мы определяем фактические маршруты. В этом случае у нас есть один маршрут: / наш домашний маршрут, который соединяется с нашим классом MainHandler. В реальном приложении у вас будут фактические маршруты, такие как / profiles или / login, привязанные к их соответствующим классам Profile и Login, каждый из которых имеет функции запроса get () и post (). В этом примере, однако, если мы отправим запрос на получение l ocalhost: 8888 /, он направит его к функции получения в классе MainHandler. Наконец, в нашей основной функции мы создаем экземпляр нашей функции routes (), назначаем ее для прослушивания порта 8888, а затем запускаем цикл ввода-вывода.
Теперь об асинхронной части. Обратите внимание на то, что функция get помечена как async перед def. Это гарантирует, что он немедленно вернет объект Future и позволит циклу ввода-вывода узнать об этом. Асинхронные функции полезны, только если есть чего ждать. В нашем примере у нас есть вымышленная функция «lot_of_data_func ()», предполагающая, что ее выполнение занимает несколько секунд. Вместо того, чтобы быть заблокированным, ожидая завершения функции, давайте отметим это ожиданием, чтобы python мог двигаться дальше и передать управление циклу ввода-вывода, чтобы программа знала, когда функция Future завершена. . Одна вещь, которая меня всегда смущала, была ожидание, похоже, что он просит код остановиться. Это полная противоположность: это позволяет функции продолжить работу и получить результат позже, когда она будет завершена. А теперь представьте, что в этом запросе функция lot_of_data_func () вызывается 5 раз. Что ж, все 5 из них будут запущены, и цикл ввода-вывода будет обрабатываться, когда каждый из них будет завершен. Намного эффективнее, чем работать над всеми 5 последовательно.
3. Алхимия Tornado SQL: чтение / запись базы данных
Вместо того, чтобы оставаться в стране фантазий, давайте перейдем к использованию в реальном мире. Запросы к базе данных - это одна из областей, загрузка которой может занять много времени, поэтому у нас есть идеальная библиотека, которая расширяет функциональность Tornado с помощью ORM (объектно-реляционное сопоставление), что упрощает получение данных. Для получения дополнительной информации об ORM и о том, как они работают, см. Этот отличный обзор.
SQL Alchemy - одна из самых популярных ORM в Python, и для удобства есть библиотека под названием tornado-sqlalchemy, которая позволяет нам использовать SQL Alchemy для асинхронной выборки или сохранения данных в нашу базу данных SQL с помощью нашей инфраструктуры торнадо. Сначала мы устанавливаем его, запустив pip install tornado-sqlalchemy или добавив tornado-sqlalchemy в ваш файл requirements.txt.
from tornado_sqlalchemy import SQLAlchemy database_url = "postgresql://user:password@localhost:port/database" def routes(): return Application([ (r"/", MainHandler), ], db=SQLAlchemy(database_url) )
Обратите внимание, что основное отличие от нашего примера заключается в том, что мы импортируем SQLAlchemy, а затем в приложение добавляем параметр db со строкой подключения к базе данных. Это связывает нас с нашей базой данных и связывает ее с маршрутами наших приложений.
Теперь мы создаем наши модели данных. Если эта часть все еще чужая, обязательно ознакомьтесь с приведенным выше обзором SQLAlchemy. Единственная разница здесь заключается в том, что для каждого класса модели мы наследуем модель SQLAlchemy нашего объекта SQLAlchemy, поскольку это то, что сопоставляет объект с нашей базой данных.
from sqlalchemy import Column, BigInteger, String from tornado_sqlalchemy import SQLAlchemy db = SQLAlchemy(url=database_url) class User(db.Model): id = Column(BigInteger, primary_key=True) username = Column(String(255), unique=True)
Наконец, давайте сделаем несколько запросов к базе данных.
from tornado_sqlalchemy import SessionMixin, as_future class MainHandler(SessionMixin, RequestHandler): async def get(self): with self.make_session() as session: count = await as_future(session.query(User).count) self.write('{} users so far!'.format(count))
Здесь мы импортируем две вещи: SessionMixin, который мы наследуем в нашем примере обработчика (и всех обработчиках запросов), который позволяет нам создать сеанс базы данных, чтобы мы могли запрашивать базу данных, и as_future, который отмечает запрос как будущее, которое мы можем ждать . Без того и другого наш запрос не будет выполняться асинхронно, что противоречит цели всей этой настройки. Итак, в приведенном выше примере мы получаем счетчик пользователей, ожидая его в будущем, цикл ввода-вывода сообщает программе, когда запрос завершен, и, наконец, возвращает окончательный результат. Бум! Асинхронные запросы к базе данных в асинхронном запросе на получение! Теперь вы можете похвастаться перед друзьями по Node.js тем, что ваши запросы на Python выполняются очень быстро.
4. Асинхронный HTTP-клиент - асинхронный запрос внешних данных
Если всего этого было недостаточно, вы также можете использовать tornado для исходящих запросов с помощью http_client tornado. Просто создайте экземпляр клиентского объекта и дождитесь функции выборки:
from tornado import httpclient async def fetch_url(): http_client = httpclient.HTTPClient() try: response = await http_client.fetch("http://www.google.com/") print(response.body) except httpclient.HTTPError as e: # HTTPError is raised for non-200 responses; the response # can be found in e.response. print("Error: " + str(e))
Здесь мы используем попытку, за исключением того, чтобы поймать не 200 ответов, но в основном единственное отличие здесь от обычной клиентской выборки - это импорт http-клиента tornado, пометка функции как асинхронная и ожидание функции client.fetch.
5. Немного лишнего бензина
Если вы действительно хотите похвастаться всеми ненавистниками питонов. Я писал о том, как ускорить работу Python с помощью Cython. Поэтому для всех ваших вычислительно-ресурсоемких функций поместите их в отдельный класс в отдельном файле, цитонизируйте этот файл, а затем импортируйте эти функции, как обычно, в файл, в котором находятся ваши классы-обработчики. Кроме того, существует библиотека под названием uvloop, которая, как и Cython, использует скомпилированный цикл C, который работает на скорости C. Вместо стандартного цикла ввода-вывода торнадо в вашей основной функции установите цикл событий как uvloop, например:
from tornado.platform.asyncio import AsyncIOMainLoop import asyncio import uvloop asyncio.set_event_loop_policy(uvloop.EventLoopPolicy()) AsyncIOMainLoop().install() asyncio.get_event_loop().run_forever()
Прохладный! Теперь ваши основные функции И ваш цикл ввода-вывода скомпилированы и работают на скорости C в рамках асинхронной ORM и веб-инфраструктуры. Наслаждаться!