Глава 11. Построение каналов взаимодействия при помощи asyncio

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

В этой главе будут рассмотрены следующие темы:

  • Основы каналов взаимодействия и применение в них асинхронного программирования

  • Как построить некий асинхронный сервер в Python при помощи asyncio b aiohttp

  • Как осуществлять асинхронные запросы ко множеству серверов и обрабатывать асинхронные чтение и запись

Технические требования

Вот перечень предварительных требований для данной главы:

  • Убедитесь что на вашем компьютере уже установлен Python 3

  • Убедитесь что на вашем компьютере уже установлен Telnet

  • Вам следует иметь установленными OpenCV и NumPy для вашего дистрибутива Python 3

  • Выгрузите необходимый репозиторий из GitHub

  • На протяжении данной главы мы будем работать с вложенной папкой, имеющей название Chapter11

  • Ознакомьтесь со следующими видеоматериалами Code in Action

Экосистема каналов взаимодействия

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

Уровни протокола взаимодействия

Основная часть процессов взаимодействия, которая осуществляется посредством каналов взаимодействия оснащается в виде модели уровней протоколов OSI (Open Systems Interconnection). Данная модель выкладывает все основные уровни и темы некоторого процесса межсистемного взаимодействия.

Следующая схема отображает общую структуру данной модели OSI:

 

Рисунок 11-1


Структура модели OSI

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

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

С другой стороны, самые верхние уровни имеют дело с взаимодействием и манипуляцией данных на верхнем уровне. Помимо этих уровней мы сосредоточимся на транспортном уровне, поскольку напрямую используется модулем asyncio в реализации каналов взаимодействия. Этот уровень обычно рассматривается как концептуальный переход между уровнями среды и верхними уровнями хостинга (к примеру, определённого клиента и конкретного сервера), отвечающего за отправку данных по всем соединениям между различными системами. Кроме того, поскольку пакеты данных (представляемые на сетевом уровне) могут теряться или нарушаться в процессе обмена из- за сетевых ошибок, транспортный уровень также в ответе за выявление таких ошибок посредством методов кодирования с обнаружением ошибок.

Прочие уровни хостинга реализуют механизмы для обработки, интерпретации и предоставления отправляемых с одной системы на другую данных. После получения данных из транспортного уровня, уровень сеанса обрабатывает процессы аутентификации, авторизации и восстановления сеанса. Уровень представления затем транслирует те же самые данные и организует их в интерпретируемое представление. Наконец, прикладной уровень отображает эти данные в дружественном пользователю виде.

Асинхронное программирование каналов взаимодействия

Отталкиваясь от самой природы асинхронного программирования, не будет удивительным что его модель программирования может предоставлять функциональность, которая действенно дополняет сам процесс содействия каналам взаимодействия. Применяя в качестве примера темы взаимодействия HTTP, определённый сервер может асинхронно обрабатывать одновременно множество клиентов; в то время как он ожидает что определённый клиент сделает некий запрос HTTP, он может переключиться на другого клиента и обработать запрос этого клиента. Аналогично, если некому клиенту требуется выполнить запросы HTTP ко множеству серверов, а при этом он должен ожидать больших откликов от некоторых серверов, он может обработать несколько откликов с малым весом, которые уже были сформированы и первыми отправлены обратно этому клиенту. Следующая схема показывает некий пример того как серверы и клиенты взаимодействуют друг с другом асинхронно в запросах HTTP:

 

Рисунок 11-2


Асинхронные, перемежающиеся запросы HTTP

Транспорт и протоколы в asyncio

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

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

DfВажно отметить, что некий объект transport в установленном канале взаимодействия из asyncio всегда ассоциирован с каким- то экземпляром класса asyncio.Protocol. Как и предполагает его название, класс Protocol определяет сами лежащие в основе протоколы, которые применяют данные каналы взаимодействия; для каждого осуществлённого с другой системой соединения будет создаваться некий новый объект протокола из данного класса. При тесной работе с неким объектом transport, объект протокола может вызывать различные методы из своего объекта transport; именно в этом месте мы можем реализовать все особенности внутренних работ какого- то канала взаимодействия.

По этой причине нам как правило необходимо сосредотачиваться на своей реализации некоторого подкласса asyncio.Protocol и его методов при построении какого- то канала взаимодействия. Иными словами, мы применяем asyncio.Protocol в качестве некоторого порождающего класса для получения некоего подкласса, отвечающего потребностям нашего канала взаимодействия Для осуществления этого мы перепишем следующие методы из asyncio.Protocol в своём собственном индивидуальном подклассе протокола:

  • Protocol.connection_made(transport): Этот метод автоматически вызывается при каждом осуществлении некоторого подключения со стороны другой системы. Соответствующий аргумент transport содержит тот объект transport, который ассоциирован с данным соединением. И опять отметим, что каждый transport требует в паре какого- то протокола; обычно мы храним такой объект transport в виде какого- то атрибута данного особого объекта протокола в соответствующем методе connection_made().

  • Protocol.data_received(data): Этот метод автоматически вызывается всякий раз, когда та самая система, к которой мы подключены, отправляет свои данные. Отметим, что соответствующий аргумент data, который содержит саму отправленную информацию, обычно представлен в байтах, следует применить функцию Python encode() прежде чем продолжить работу с data.

Далее мы рассмотрим наиболее важные методы из класса обмена asyncio. Все классы обмена наследуются из некоторого родительского класса обмена, имеющего название asyncio.BaseTransport, для которого у нас имеются следующие общие методы:

  • BaseTransport.get_extra_info(): Этот метод, как и предполагает его название, возвращает дополнительную информацию относительно канала для самого вызвавшего его объекта transport. Полученный результат может содержать информацию, относящуюся к соответствующему сокету, определённому конвейеру и тому подпроцессу, который ассоциирован с данным обменом. Позднее в этой главе мы будем вызывать BaseTransport.get_extra_info('peername') для того, чтобы получать необходимый удалённый адрес с которого доставлен этот обмен.

  • BaseTransport.close(): Данный метод используется для закрытия соответствующего вызывающего объекта transport, после чего все соединения между различными системами будут остановлены. Сам соответствующий протокол данного обмена автоматически вызывает свой метод connection_lost().

Удалившись от слишком большого числа реализаций классов транспорта, мы сосредоточимся на определённом классе asyncio.WriteTransport, который вновь наследует свои методы из основного класса BaseTransport, а также дополнительно реализует иные методы, которые мы используем для оснастки функциональностей обмена с доступом только для записи. В данном случае мы будем применять метод WriteTransport.write(), который будет записывать те данные , которые мы бы хотели отправить в другую подключённую систему, с которой мы взаимодействуем через соответствующий объект transport. Выступая в качестве части модуля asyncio, этот метод является не блокирующей функцией; вместо этого он выполняет буферизацию и отправку необходимых записываемых данных неким асинхронным манером.

Клиент сервера asyncio крупными мазками

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

Как мы уже упоминали ранее, нам требуется реализовать некий подкласс asyncio.Protocol для описания необходимой лежащей в основе организации своего канала взаимодействия. И снова, в сердце всякой асинхронной программы имеется некий цикл событий, следовательно нам необходимо создать некий сервер вовне имеющегося контекста своего класса протокола и инициировать данный сервер внутри имеющегося в нашей программе цикла событий. Этот процесс настроит требуемую асинхронную архитектуру нашего сервера целиком и может быть осуществлён через соответствующий метод asyncio.create_server(), который мы увидим в своём грядущем примере.

Наконец, мы запустим необходимый цикл событий в бесконечном цикле при помощи метода AbstractEventLoop.run_forever(). Аналогично некому действительному серверу из реальной жизни, мы бы хотели сохранять свой сервер запущенным до тех пор пока он не столкнётся с какой- то проблемой, и в этом случае мы аккуратно закроем этот сервер. Приводимая ниже схема иллюстрирует данный процесс целиком:

 

Рисунок 11-3


Структура асинхронной программы в каналах взаимодействия

Пример Python

Теперь мы рассмотрим некий частный пример Python реализации сервера, который оказывает содействие асинхронному взаимодействию. Выгрузите сопроводительный код этой книги с нашей страницы GitHub и перейдите в папку Chapter11.

Запуск сервера

В файле Chapter11/example1.py давайте рассмотрим следующий класс EchoServerClientProtocol:


# Chapter11/example1.py

import asyncio

class EchoServerClientProtocol(asyncio.Protocol):
    def connection_made(self, transport):
        peername = transport.get_extra_info('peername')
        print('Connection from {}'.format(peername))
        self.transport = transport

    def data_received(self, data):
        message = data.decode()
        print('Data received: {!r}'.format(message))
 	   

В данном случае наш класс EchoServerClientProtocol является неким подклассом asyncio.Protocol. Как мы уже обсуждали ранее, внутри этого класса нам следует реализовать свои методы connection_made(transport) и data_received(data). В своём методе connection_made() мы просто получаем необходимый адрес подключённой системы через метод get_extra_info() ( с аргументом 'peername'), выводим на печать сообщение с этой информацией и наконец сохраняем полученный объект transport в некотором атрибуте данного класса. Для вывода на печать аналогичного сообщения в методе data_received() мы вновь применяем метод decode() для получения объекта строки из данных в байтах.

Давайте перейдём к основной программе своего сценария, вот она:


# Chapter11/example1.py

loop = asyncio.get_event_loop()
coro = loop.create_server(EchoServerClientProtocol, '127.0.0.1', 8888)
server = loop.run_until_complete(coro)

# Serve requests until Ctrl+C is pressed
print('Serving on {}'.format(server.sockets[0].getsockname()))
try:
    loop.run_forever()
except KeyboardInterrupt:
    pass

# Close the server
server.close()
loop.run_until_complete(server.wait_closed())
loop.close()
 	   

Мы применяем уже знакомую нам функцию asyncio.get_event_loop() для создания некоторого цикла событий в своей асинхронной программе. Затем мы создаём некий сервер для своего взаимодействия имея вызов соответствующего метода create_server() такого цикла событий; данный метод получает некий подкласс из имеющегося класса asyncio.Protocol, некий адрес для нашего сервера (в данном случае это наш локальный хост, 127.0.0.1) и, наконец, какой- то порт для этого адреса (обычно 8888).

Отметим, что данный метод не создаёт соответствующий сервер сам по себе; он всего лишь инициирует необходимый процесс асинхронного создания такого сервера и возвращает какую- то сопрограмму, которая завершит данный процесс. По этой причине нам требуется сохранить эту самую возвращаемую сопрограмму из данного метода в какой- то переменной (в нашем случае coro) и заставить наш цикл событий запустить эту сопрограмму. После вывода сообщения с применением атрибута sockets нашего объекта сервера, мы запустим свой цикл событий навсегда чтобы сохранять наш сервер в запущенном состоянии за исключением того случая, когда будет вызвана исключительная ситуация KeyboardInterrupt.

Наконец, в самом конце своей программы мы обработаем часть домашней уборки своего сценария, которая аккуратно закрывает наш сервер. Это обычно делается путём вызова метода closed() соответствующего объекта сервера (для инициации собственно процесса закрытия данного сервера), а также с применением запуска метода wait_closed() самого цикла событий для данного объекта сервера чтобы убедиться в надлежащем закрытии сервера. В конце концов, мы закрываем сам цикл событий.

Установка Telnet

Прежде чем мы получим возможность запустить пример своей программы Python, нам требуется установить программу Telnet для корректной имитации некоторого подключения канала между какими- то клиентом и сервером. Telnet является программой, которая предоставляет команды Терминала, которые оказывают содействие протоколами для двунаправленного, интерактивного взаимодействия, ориентированного на текст. Если у вас уже имеется работающий Telnet в вашем компьютере, просто перейдите к следующему разделу; в противном случае отыщите в этом разделе информацию, подходящую для вашей системы.

В системах Windows Telnet уже установлен, но может быть отключён. Для его включения вы можете либо воспользоваться окном Turn Windows features on or off и убедиться что блочок с Telnet Client помечен, или исполнить такую команду:


dism /online /Enable-Feature /FeatureName:TelnetClient
		

Linux системы обычно поступают с предварительно установленным Telnet, поэтому если вы владелец системы Linux просто перейдите к следующему разделу.

В системах macOS имеется вероятность что Telnet уже установлен в вашем компьютере. Если это не так, вам придётся сделать это посредством программного обеспечения управления пакетами Homebrew следующим образом:


brew install telnet
		

Отметим также, что системы macOS имеют также предварительно установленным некую альтернативу Telnet с названием Netcat. Если вы не желаете устанавливать Telnet в своём компьютере macOS, просто воспользуйтесь командой nc вместо telnet в наших последующих примерах и вы достигните того же самого эффекта.

Имитация канала соединения

Есть ещё много шагов для запуска нашего следующего примера сервера. Во- первых нам требуется запустить свой сервер, от которого мы получаем следующий вывод:


> python example1.py
Serving on ('127.0.0.1', 8888)
		

Отметим, что данная программа будет исполняться пока не будет вызвано нажатием комбинации Ctrl + C. При наличии соответствующей программы всё ещё запущенной в одном из Терминалов (это Терминал нашего сервера), откройте ещё один Терминал и подключитесь к своему серверу (127.0.0.1) по определённому вами порту (8888); он будет обслуживать наш Терминал клиента:


telnet 127.0.0.1 8888
		

Теперь вы можете отметить некоторые изменения как в Терминале сервера, так и в Терминале клиента. Скорее всего, Терминал вашего клиента имеет следующий вывод:


> telnet 127.0.0.1 8888
Trying 127.0.0.1...
Connected to localhost.
		

Это получено из интерфейса программы Telnet, и оно указывает что мы успешно выполнили подключение к своему локальному серверу. Более интересен вывод в Терминале нашего сервера и он будет аналогичен такому:


> python example1.py
Serving on ('127.0.0.1', 8888)
Connection from ('127.0.0.1', 60332)
		

Напомним, что это некое информационное сообщение, которое мы реализовали в своём классе EchoServerClientProtocol - а именно, в методе connection_made(). Кроме того, так как было выполнено некое подключение к нашему серверу и был создан новый клиент, этот метод будет вызываться автоматически для инициации данного взаимодействия. Из данного сообщения на выходе мы узнаём, что наш клиент делал свои запросы с порта 60332 к серверу 127.0.0.1 (который является нашим исполняемым сервером, так как они оба расположены локально).

Другой реализованной нами функциональностью в нашем классе EchoServerClientProtocol был метод data_received(). А именно, мы выводим на печать декодированные данные, которые мы отправили со своего клиента. Для имитации данного типа взаимодействия просто наберите какое- то сообщение в Терминале своёго клиента и нажмите клавишу Return (Enter для Windows). Вы не увидите никаких изменений в выводе терминала своего клиента, но Терминал сервера должен вывести на печать некое сообщение, что предписано методом data_received() нашего класса протокола.

Например, вот что происходит в выводе Терминала моего сервера когда я отправил сообщение Hello, World! из Терминала своего клиента:


> python example1.py
Serving on ('127.0.0.1', 8888)
Connection from ('127.0.0.1', 60332)
Data received: 'Hello, World!\r\n'
		

Символы \r\n это просто признак конца строки, включённый в данную строку сообщения. При помощи нашего текущего протокола вы можете отправлять сообщения в свой сервер и даже можете иметь множество клиентов, отправляющих сообщения в этот сервер. Чтобы реализовать это просто откройте ещё одно терминальное окно и снова подключитесь к своему серверу. В Терминале своего сервера вы заметите что другой клиент (с другого порта) выполнил подключение к этому серверу, в то время как первоначальное взаимодействие нашего сервера со своим старым клиентом всё ещё поддерживается. Именно это является ещё одним результатом достигаемым асинхронным программированием, возможность множеству клиентов бесшовно взаимодействовать с одним и тем же сервером, без какого бы то ни было применения потоков и множества процессов.

Отправка сообщений обратно клиентам

Итак, в своём текущем примере мы способны заставить свой сервер получать, считывать и обрабатывать сообщения от клиентов. Тем не менее, чтобы наш канал взаимодействия был полезным, нам также требуется отправлять сообщения от своего сервера его клиентам. В данном разделе мы обновим свой сервер до некоторого сервера эхо который, по определению, будет отправлять все те данные которые он получает от конкретного клиента обратно этому клиенту.

Для этого мы будем применять метод write() из стандартного класса asyncio.WriteTransport. Изучите свой файл Chapter11/example2.py на предмет следующего метода data_received() из класса EchoServerClientProtocol:


# Chapter11/example2.py

import asyncio

class EchoServerClientProtocol(asyncio.Protocol):
    def connection_made(self, transport):
        peername = transport.get_extra_info('peername')
        print('Connection from {}'.format(peername))
        self.transport = transport

    def data_received(self, data):
        message = data.decode()
        print('Data received: {!r}'.format(message))

        self.transport.write(('Echoed back: {}'.format(message)).encode())

loop = asyncio.get_event_loop()
coro = loop.create_server(EchoServerClientProtocol, '127.0.0.1', 8888)
server = loop.run_until_complete(coro)

# Serve requests until Ctrl+C is pressed
print('Serving on {}'.format(server.sockets[0].getsockname()))
try:
    loop.run_forever()
except KeyboardInterrupt:
    pass

# Close the server
server.close()
loop.run_until_complete(server.wait_closed())
loop.close()
 	   

После получения данных из объекта transport и вывода их на печать, мы записываем соответствующее сообщение в этот объект transport, который будет возвращён обратно своему первоначальному клиенту. Запустив сценарий Chapter11/example2.py и проэмулировав то же самое взаимодействие, которое мы реализовали в своём последнем примере при помощи Telnet или Netcat, вы обнаружите, что после набора какого- то сообщения в Терминале своего клиента, этот клиент получает это сообщение в виде эхо от своего сервера. Ниже приводится мой вывод после инициализации соответствующего канала взаимодействия и набора мной сообщения Hello, World!:


> telnet 127.0.0.1 8888
Trying 127.0.0.1...
Connected to localhost.
Hello, World!
Echoed back: Hello, World!
		

По- существу, данный пример иллюстрирует наличие возможности двунаправленного канала взаимодействия, который мы реализовали посредством некоторого индивидуального класса asyncio.Protocol. Пока запущен некий сервер, мы имеем возможность получать данные, отправляемые различными подключёнными к этому серверу клиентами, обрабатывать такие данные, и, наконец, отправлять желаемый результат обратно соответствующим клиентам.

Закрытие используемого транспорта

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

Поэтому, вместо того чтобы оставлять открытыми имеющиеся взаимодействия для всех и каждого подключённых к этому серверу клиентов, мы можем определить в своём протоколе что каждое подключение следует закрывать после успешного взаимодействия. Мы будем делать это при помощи соответствующего метода BaseTransport.close() для принудительного закрытия соответствующего вызванного объекта transport, что остановит данное соединение между нашим сервером и этим клиентом. И вновь мы изменим свой метод data_received() из нашего класса EchoServerClientProtocol в Chapter11/example3.py следующим образом:


# Chapter11/example3.py

import asyncio

class EchoServerClientProtocol(asyncio.Protocol):
    def connection_made(self, transport):
        peername = transport.get_extra_info('peername')
        print('Connection from {}'.format(peername))
        self.transport = transport

    def data_received(self, data):
        message = data.decode()
        print('Data received: {!r}'.format(message))

        self.transport.write(('Echoed back: {}'.format(message)).encode())

        print('Close the client socket')
        self.transport.close()

loop = asyncio.get_event_loop()
coro = loop.create_server(EchoServerClientProtocol, '127.0.0.1', 8888)
server = loop.run_until_complete(coro)

# Serve requests until Ctrl+C is pressed
print('Serving on {}'.format(server.sockets[0].getsockname()))
try:
    loop.run_forever()
except KeyboardInterrupt:
    pass

# Close the server
server.close()
loop.run_until_complete(server.wait_closed())
loop.close()
 	   

Исполните этот сценарий, попытайтесь выполнить соединение к заданному серверу и наберите какое- то сообщение чтобы знакомиться с теми изменениями, которые мы реализовали. Для текущей установки после того как некий клиент подключается и отправляет какое- то сообщение в свой сервер, он получит в виде эхо своё сообщение обратно, а его подключение к данному серверу будет закрыто. Ниже приводится вывод ( и опять из интерфейса нашей программы Telnet), который я получаю после имитации данного процесса со своей текущей реализацией данного протокола:


> telnet 127.0.0.1 8888
Trying 127.0.0.1...
Connected to localhost.
Hello, World!
Echoed back: Hello, World!
Connection closed by foreign host.
		

Взаимодействие стороны клиента с aiohttp

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

Как мы могли предвосхитить, наша конечная цель данного процесса состоит в действенном сборе данных от внешних систем путём выполнения асинхронных запросов к этим системам. Мы будем пересматривать основную концепцию веб сбора информации (скрапинга), который состоит в процессе автоматизации запросов HTTP к различным вебсайтам и выделении конкретной информации из их исходных кодов HTML. Если вы ещё не читали Главу 5, Одновременные веб запросы, я настоятельно рекомендую пройтись по ней прежде чем продолжить данный раздел, так как эта глава рассматривает основополагающие идеи сбора веб информации и прочих относящихся к этому понятий.

В этом разделе мы также предложим введение в прочие модули, которые поддерживают варианты асинхронного программирования: aiohttp (что является сокращением от Asynchronous I/O HTTP). Этот модуль предоставляет функциональность верхнего уровня, которая рационализирует процедуры взаимодействия HTTP, а также бесшовно работает с обсуждаемым нами модулем asyncio для содействия асинхронному программированию.

Установка aiohttp и aiofiles

Рассматриваемый нами модуль aiohttp не поставляется предварительно установленным в вашем дистрибутиве Python; тем не менее, аналогично прочим пакетам вы запросто можете установить этот модуль воспользовавшись соответствующими командами pip или conda. Мы также установим и другой модуль, aiofiles который содействует асинхронной записи в файлы. Если вы применяете в качестве своего диспетчера пакетов pip, просто запустите такие команды:


pip install aiohttp
pip install aiofiles
		

Если вы применяете Anaconda, запустите следующие команды:


conda install aiohttp
conda install aiofiles
		

Как и всегда, чтобы убедиться что вы успешно установили некий пакет, откройте свой интерпретатор Python и попробуйте выполнить импорт соответствующего модуля. В данном случае выполните такой код:


>>> import aiohttp
>>> import aiofiles 
		

Если эти пакеты были успешно установлены, не будет никаких сообщений об ошибках.

Выборка кода HTML с вебсайта

Прежде всего, давайте рассмотрим как выполнить некий запрос и получить соответствующий исходный код HTML с отдельного вебсайта при помощи aiohttp. Отметим, что даже только для одной задачи (одного вебсайта) наше приложение остаётся асинхронным и всё ещё требуется реализовать всю инфраструктуру некоторой асинхронной программы. Теперь перейдите к следующему файлу Chapter11/example4.py:


# Chapter11/example4.py

import aiohttp
import asyncio

async def get_html(session, url):
    async with session.get(url, ssl=False) as res:
        return await res.text()

async def main():
    async with aiohttp.ClientSession() as session:
        html = await get_html(session, 'http://packtpub.com')
        print(html)

loop = asyncio.get_event_loop()
loop.run_until_complete(main())
 	   

Давайте первой рассмотрим свою сопрограмму main(). Мы инициализируем некий экземпляр из класса aiohttp.ClientSession внутри какого- то диспетчера контекста; отметим, что мы также помещаем ключевое слово async перед этим объявлением, так как весь этот блок контекста сам по себе также будет рассматриваться как некая сопрограмма. Внутри данного блока мы делаем вызов и дожидаемся обработки и возврата своей сопрограммы get_html().

Переключив своё внимание на данную сопрограмму get_html(), мы можем обнаружить, что она получает некий объект сеанса и URL для вебсайта, с которого мы намерены выделить необходимый исходный код HTML. Внутри данной функции мы делаем асинхронным другой диспетчер контекста, который применяется для исполнения запроса GET и сохранения получаемого от сервера отклика в переменной res. Наконец, мы возвращаем необходимый исходный код HTML, сохранённый в нашем отклике в виде некоторого объекта, возвращаемого из нашего класса aiohttp.ClientSession, причём её методы являются асинхронными функциями и, следовательно, нам необходимо определить ключевое слово await когда мы вызываем свою функцию text().

После того как вы исполните данную программу, на печать будет выведен соответствующий исходный код HTML вебсайта Packt целиком. Например, ниже приводится некая часть моего вывода:

 

Рисунок 11-4


Исходный код HTML из aiohttp

Асинхронная запись файлов

По большей части было бы неприемлемо для нас собирать данные выполняя запросы ко множеству вебсайтов и просто выводить на печать получаемый в ответ код HTML; вместо этого мы бы желали записывать такой ответный код HTML в файлы вывода. По существу это процесс асинхронной выгрузки, который также реализован в лежащей в их основе архитектуры популярных диспетчеров выгрузки. Для того чтобы осуществить всё это мы воспользуемся модулем aiofiles в сочетании с aiohttp и asyncio.

Перейдём к файлу Chapter11/example5.py. Вначале мы рассмотрим такую сопрограмму download_html():


# Chapter11/example5.py

async def download_html(session, url):
    async with session.get(url, ssl=False) as res:
        filename = f'output/{os.path.basename(url)}.html'

        async with aiofiles.open(filename, 'wb') as f:
            while True:
                chunk = await res.content.read(1024)
                if not chunk:
                    break
                await f.write(chunk)

        return await res.release()
 	   

Это некая обновлённая версия сопрограммы get_html() из нашего самого последнего примера. Вместо того чтобы применять некий экземпляр aiohttp.ClientSession для выполнения запроса GET и вывода на печать возвращаемого кода HTML, теперь мы записываем этот код HTML в соответствующий файл при помощи модуля aiofiles. К примеру, для сопровождения асинхронной записи файла мы применяем функцию open() из aiofiles для считывания некоторого файла в каком- то диспетчере контекста. Далее мы считываем порции возвращаемого HTML, причём асинхронно, пользуясь функцией read() для атрибута content в соответствующем объекте отклика; это означает, что после считывания 1024 байт нашего текущего отклика поток исполнения будет высвобождаться обратно своему циклу событий и будет происходить событие переключения задач.

Сама сопрограмма main() и наша основная программа в данном примере остаются относительно теми же самыми что и в нашем последнем примере:


async def main(url):
    async with aiohttp.ClientSession() as session:
        await download_html(session, url)

urls = [
    'http://packtpub.com',
    'http://python.org',
    'http://docs.python.org/3/library/asyncio',
    'http://aiohttp.readthedocs.io',
    'http://google.com'
]

loop = asyncio.get_event_loop()
loop.run_until_complete(
    asyncio.gather(*(main(url) for url in urls))
 	   

Наша сопрограмма main() получает некий URL и передаёт его в другую сопрограмму download_html(), причём совместно с неким экземпляром aiohttp.ClientSession. Наконец, в своей основной программе мы создаём некий цикл событий и передаём все элементы из определённого перечня URL в соответствующую сопрограмму main(). После запуска данной программы ваш вывод должен выглядеть аналогично такому, хотя время исполнения данной программы может и быть иным:


> python3 example5.py
Took 0.72 seconds.
		

Дополнительно к этому некая вложенная папка с названием output (внутри нашей папки Chapter11) будет заполнена множеством выгруженных кодов HTML с каждого из вебсайтов в нашем перечне URL. И напомним, что все эти файлы создавались и записывались асинхронно, пользуясь основой заложенной в модуле aiofiles, которую мы обсудили ранее. Как вы можете заметить, для сопоставления скорости данной программы и её соответствующей синхронной версии, мы также отслеживаем то время, которое занимает исполнение всей программы целиком.

Теперь проследуем к своему файлу Chapter11/example6.py. Этот сценарий содержит код синхронной версии той же самой программы. В частности, он выполняет запросы HTTP GET к индивидуальным вебсайтам по порядку, а процесс записи файлов также выполняется последовательно. Данный сценарий производит такой вывод:


> python3 example6.py
Took 1.47 seconds.
		

Хотя она и достигает того же самого результата (выгрузки необходимого кода HTML и записи его в файлы), наша последовательная программа занимает намного больше времени чем её асинхронный конкурент.

Выводы

Существует семь основных уровней взаимодействия в процессе обмена данными, причём с различными степенями вычислительного уровня. Уровни среды содержат достаточно низкоуровневые операции которые взаимодействуют с процессами, составляющими основу канала взаимодействия, в то время как уровни хостинга имеют дело с высокоуровневыми взаимодействиями и манипуляциями данных. Из всех семи, именно транспортный уровень обычно рассматривается как в качестве основного концептуального перехода от уровней среды к уровням хостинга, отвечая за пересылку данных по всем подключениями между различными системами. Асинхронное программирование может предоставлять функциональности, которые дополняют сам процесс эффективного сопровождения каналов взаимодействия.

Сопровождая сервер, модуль asyncio сочетает свои абстракции обмена с реализацией некоторой асинхронной программы. В частности через посредничество классов BaseTransport и BaseProtocol, asyncio предоставляет различные способы индивидуализации лежащей в основе архитектуры некоторого канала взаимодействия. Совместно с модулем aiohttp, asyncio предлагает действенные и гибкие процессы, относящиеся к взаимодействию со стороны клиента. Следующий модуль, aiofiles, который может работать в сочетании с прочими двумя обсуждёнными асинхронными модулями программирования, также может оказывать содействие асинхронным чтению и записи файлов.

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

Вопросы

  • Что представляет из себя канал взаимодействия? В чём состоит его связь с асинхронным программированием?

  • Что составляет две основные части модели OSI уровней протоколов? Какие цели обслуживает каждая из них?

  • Что представляет из себя транспортный уровень? Почему он критически важен для каналов взаимодействия?

  • Как asyncio способствует реализации каналов взаимодействия стороны сервера?

  • Как asyncio способствует реализации каналов взаимодействия стороны клиента?

  • Что такое aiofiles?

Дальнейшее чтение

Для получения дополнительных сведений вы можете воспользоваться следующими ссылками: