Все мы пользуемся различными приложениями для разных целей. Банковские приложения для операций с денежными средствами, мессенджеры для общения. Они принимают внутрь себя команду от человека и возвращают ответ. Банальный запрос ответ, но это так кажется с первого взгляда. Эти операции называются Input Output и они является самой распространённой операцией в сети. Предлагаю сегодня разобраться как они работают.
Определение
Операции ввода-вывода (I/O) — это операции, которые связанны с получением или отправкой данных, они требуют взаимодействия с внешними источниками (например, файловой системой, сетью, базой данных, сервером и т.д.). В контексте сетевого I/O они не используют процессор напрямую.
В блокирующем/синхронном режиме они блокируют выполнение программы в ожидании ответа от удалённого сервера или устройства.
В неблокирующем/асинхронном режиме они не блокируют выполнения программы в ожидании ответа а занимаются отправкой нового запроса.
Классификация IO
Грубо можно разделить на несколько групп, так как официальной классификации мною не было найдено:
-
File IO — работает с файлами и каталогами на диске через файловую систему;
-
Device IO — работа с данными физических устройств: клавиатуры, мыши, диски, …;
-
Console IO — работает с вводом/выводом в терминале;
-
Network IO — работает с данными по сети через сетевые сокеты;
-
Inter-Process IO — работает с данными между процессами в пределах одного устройства;
Инструменты IO
В Python существует множество библиотек для работы с различным IO. Каждая из них имеет свои особенности и области применения. Перечислять все библиотеки для каждого IO мы не будем, расскажем про самые популярные и чуть-чуть затронем Linux утилиты.
|
Инструмент |
Прикладной протокол |
Транспортный протокол |
Тип |
Применение |
|
HTTP |
TCP |
Network IO |
Синхронная Python библиотека для работы с веб-ресурсами. |
|
|
HTTP |
TCP |
Network IO |
Асинхронная Python библиотека для работы с веб-ресурсами. |
|
|
HTTP |
TCP |
Network IO |
Асинхронная/синхронная Python библиотека для работы с веб-ресурсами. |
|
|
FTP |
TCP |
Network IO |
Синхронная Python библиотека для работа с FTP-серверами. |
|
|
SSH |
TCP |
Network IO |
SSH Python клиент. |
|
|
TCP |
Network IO |
Асинхронный Python драйвер для СУБД PostgreSQL. |
||
|
TCP |
Network IO |
Cинхронный Python драйверы для СУБД MySQL. |
||
|
— |
— |
File IO |
Асинхронная Python библиотека для работы с файлами. |
|
|
— |
— |
File IO |
Синхронная Python функция для чтения файлов. |
|
|
touch, cat, more |
— |
— |
Console IO |
Синхронные UNIX утилиты для работы с файлами. |
|
— |
— |
Inter-Process IO |
Работа с общей памятью между процессами на одном устройстве. |
|
|
— |
— |
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/
Добавить комментарий