Операции ввода-вывода в Python

от автора

Все мы пользуемся различными приложениями для разных целей. Банковские приложения для операций с денежными средствами, мессенджеры для общения. Они принимают внутрь себя команду от человека и возвращают ответ. Банальный запрос ответ, но это так кажется с первого взгляда. Эти операции называются Input Output и они является самой распространённой операцией в сети. Предлагаю сегодня разобраться как они работают.

Определение

Операции ввода-вывода (I/O) — это операции, которые связанны с получением или отправкой данных, они требуют взаимодействия с внешними источниками (например, файловой системой, сетью, базой данных, сервером и т.д.). В контексте сетевого I/O они не используют процессор напрямую.

В блокирующем/синхронном режиме они блокируют выполнение программы в ожидании ответа от удалённого сервера или устройства.

В неблокирующем/асинхронном режиме они не блокируют выполнения программы в ожидании ответа а занимаются отправкой нового запроса.

Классификация IO

Грубо можно разделить на несколько групп, так как официальной классификации мною не было найдено:

  • File IO — работает с файлами и каталогами на диске через файловую систему;

  • Device IO — работа с данными физических устройств: клавиатуры, мыши, диски, …;

  • Console IO — работает с вводом/выводом в терминале;

  • Network IO — работает с данными по сети через сетевые сокеты;

  • Inter-Process IO — работает с данными между процессами в пределах одного устройства;

Инструменты IO

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

Инструмент

Прикладной протокол

Транспортный протокол

Тип

Применение

requests

HTTP

TCP

Network IO

Синхронная Python библиотека для работы с веб-ресурсами.

aiohttp

HTTP

TCP

Network IO

Асинхронная Python библиотека для работы с веб-ресурсами.

httpx

HTTP

TCP

Network IO

Асинхронная/синхронная Python библиотека для работы с веб-ресурсами.

ftplib

FTP

TCP

Network IO

Синхронная Python библиотека для работа с FTP-серверами.

paramiko

SSH

TCP

Network IO

SSH Python клиент.

psycopg2

Frontend/Backend Protocol

TCP

Network IO

Асинхронный Python драйвер для СУБД PostgreSQL.

PyMySQL

MySQL Protocol

TCP

Network IO

Cинхронный Python драйверы для СУБД MySQL.

aiofiles

File IO

Асинхронная Python библиотека для работы с файлами.

open

File IO

Синхронная Python функция для чтения файлов.

touch, cat, more

Console IO

Синхронные UNIX утилиты для работы с файлами.

shared_memory

Inter-Process IO

Работа с общей памятью между процессами на одном устройстве.

pyserial

Device IO

Работа с портами и интерфейсами.

Все эти библиотеки предоставляют удобный интерфейс для работы с разными источниками: с файлами, веб-ресурсами, СУБД, серверами, портами, памятью.

В рамках данной статьи мы рассмотрим только Network IO.

Принцип работы Network IO

Рассмотрим как оно работает максимально подробно насколько нам это позволит Python. Для демонстрации будем использовать это API: http://jsonplaceholder.typicode.com/posts/1 Ниже представлен код, который будет разобран по шагам.

import socket  class Fetch:      def __init__(self):         self._port = 80         self.path = "/posts/1"         self.host = "jsonplaceholder.typicode.com"         self.response = b""         self._family = socket.AF_INET         self._type = socket.SOCK_STREAM      def __call__(self) -> bytes:         ip = self.request_to_dns()          # 1         request = self.create_request()     # 2         sock = self.create_socket()         # 3         sock.connect((ip, self._port))      # 4         sock.sendall(request)               # 5         self.read_response(sock)            # 6         sock.close()         print("Socket is closed\n")         return self.response      def request_to_dns(self) -> str:         response = socket.getaddrinfo(self.host, self._port, self._family, self._type)         print(f"Response from dns: {response}")         first_tuple = response[0]         print(f"First tuple in response from dns: {first_tuple}")         ip = first_tuple[4][0]         print(f"Resolved: {self.host} -> {ip}:{self._port}")         return ip      def create_socket(self):         sock = socket.socket(self._family, self._type)         print("Socket created")         return sock      def create_request(self) -> bytes:         headers = [f"Host: {self.host}", "Connection: close"]         http_request = f"GET {self.path} HTTP/1.1\r\n" + "\r\n".join(headers) + "\r\n\r\n"         print(f"Create HTTP request")         return http_request.encode()      def read_response(self, sock):         print("Wait for response")         while True:             chunk = sock.recv(4096)             if not chunk:                 break             self.response += chunk  print(Fetch()().decode())

Шаг первый — запрос в DNS

Прежде чем получить информацию от Web ресурса нам сначала необходимо получить все доступные его адреса, которые хранятся в DNS. Это можно сделать с помощью функции socket.getaddrinfo которая является оберткой над libc, стандартной библиотекой языка Си. Для автоматизации процесса в функцию передаются все данные о будущем соединении в виде параметров. Таким образом можно получить сразу все подходящие адреса(IPv4 или IPv6) для запроса.

Параметры socket.getaddrinfo:

  • family — Семейство адресов(socket.AF_INET (IPv4), socket.AF_INET6 (IPv6), etc);

  • type — Тип сокета(socket.SOCK_STREAM (TCP), socket.SOCK_DGRAM (UDP), etc);

  • proto — Указания конкретного протокола(socket.IPPROTO_TCP, socket.IPPROTO_UDP);

  • flags — Флаги управления поведением запроса;

Шаг второй — создание запроса

Данный шаг создает специальное HTTP сообщение, потому что будет использован прикладной протокол HTTP (версии 1.1).

Ниже представлен абстрактный пример сообщения.

GET /articles/42 HTTP/1.1       // Request line. Хранит метод, путь и версию протокола. Host: example.com               Host: example.com               // Обязательный заголовок в HTTP/1.1. Содержит доменное имя сервера. Connection: close               // Указывает серверу, что после ответа соединение можно закрыть. User-Agent: CustomClient/1.0    // Заголовок, сообщающий серверу, кто делает запрос(Iphone, Bot, Mac, etc).  Accept: application/json        // Указывает, что клиент готов принять любой тип содержимого в ответ. ...                             // Еще какие-то заголовки.                                 // Пустая строка обязательный разделитель головы и тела запроса. name=John+Doe&age=30&city=NY    // Тело запроса. Для GET запроса обычно не используется.

Сообщение необходимо серверу для понимания что мы хотим от него:

  • Закрывать ли сразу соединение?

  • В какой хост мы отправляем запрос?

  • В какое конкретно API мы обращаемся?

  • Какой формат данных мы можем обработать?

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

  • Что мы хотим сделать(GET — получить данные или POST — опубликовать данные)?

Когда сообщение готово, нам необходимо его преобразовать в байты.

Шаг третий — создание сокета

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

При создании сокета мы передали два параметра:для надёжной

  • AF_INET — Число 2, означает что будет использовано адресное семейство IPv4.

  • SOCK_STREAM — Число 1, означает, что будет использован протокол TCP, а не UDP.

Шаг четвертый — установка соединения

Для установки соединения с сервером используется метод sock.connect в который необходимо передать адрес сервера и его порт. Далее на уровне TCP происходит трёхстороннее рукопожатие для установки соединение по которому далее будет передано сообщение.

Шаг пятый — отправка сообщения

Закодированное в байты сообщение отправляется по кускам серверу и чтобы всё сообщение было отправлено используется метод sock.sendall(request).

Шаг шестой — получения ответа

Так как не возможно предугадать сколько конкретно байтов придет от сервера, то есть длинна ответа неизвестна, необходимо с помощью цикла и socket.recv(size) получать ответ по кусочка. Примерный размеро кусочков сообщения будет 4 кб. Постепенно наполняя атрибут экземпляра класса self.response данными.

В данном клиенте, для получния данных, используется size=4096 и это распространённый размер партиции сообщения для чтения сетевых данных, а не абстрактная цифра.

Когда передача сообщения сервером закончено он передает пустой байтовый объект b'' что является сигналом о завершении передачи.

Возможные вопросы:

Как получить все IP адреса с помощью socket.getaddrinfo?

Это довольно просто, передайте в функцию всего два параметра: host и port. Как в примере ниже:

import socket from pprint import pprint  response = socket.getaddrinfo(host="jsonplaceholder.typicode.com", port=80)  ipv4, ipv6 = set(), set() for address_tuple in response:     address = address_tuple[-1][0]     if "." in address:         ipv4.add(address)         continue     ipv6.add(address)  print("IPv4") pprint(ipv4) print() print("IPv6") pprint(ipv6)

Как получить IP адреса без Python?

Для получения IP адресов с помощью терминала в Linux стоит использовать утилиту dig.Так как обычным HTTP GET запросом это сделать не возможно. Потому что для такого запроса требуется специальный протокол. Называется он DNS-протоколом.

Команда: dig jsonplaceholder.typicode.com +short

Заключение

В данной статье мы рассмотрели, что такое операции ввода-вывода (IO). Особое внимание было уделено сетевому вводу-выводу (Network IO) и тому, как он работает на низком уровне с использованием стандартной библиотеки socket.

Разобрав пример с ручной реализацией HTTP-запроса, мы на практике прошли все этапы использования сетевого соединения: от разрешения DNS-имени до получения ответа от сервера.

Такой подход позволяет лучше понять, как устроены высокоуровневые библиотеки (например, requests, aiohttp) и что происходит «под капотом» при работе с сетью.

Возможно некоторые скажут что статья не совсем полная, так как тут не затрагивается тема защищенного протокола HTTP, не совсем подробно рассказано про DNS, не затронуты темы как это работает в Linux. Но задача данной статьи дать самое базовое понимание как работает NetworkIO.

Если вам понравилась статья, вы можете посмотреть маленькие мини-рубрики в моем Telegram канале.


ссылка на оригинал статьи https://habr.com/ru/articles/928694/


Комментарии

Добавить комментарий

Ваш адрес email не будет опубликован. Обязательные поля помечены *