Как создать полностью асинхронный сервер приложений на Python

  1. Введение в асинхронное программирование

Асинхронное программирование может быть немного сложным со всем его жаргоном, но давайте разберем его простыми словами. Думайте об этом, как о выполнении множества дел по дому. Вы можете выполнять их по одному и дождаться завершения каждого, прежде чем начинать следующий (синхронно), или вы можете запустить один, а затем запустить другой, пока идет первый. Представьте, что вы начинаете стирку. Для вас не имеет смысла ждать, пока он закончится, прежде чем начинать готовить ужин. Гораздо эффективнее начать стирку в стиральной машине, а затем приготовить ужин, пока работает прачечная.

По сути, это то, что делает синтаксис 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 и веб-инфраструктуры. Наслаждаться!