Все мы пользуемся различными приложениями для разных целей. Банковские приложения для операций с денежными средствами, мессенджеры для общения. Они принимают внутрь себя команду от человека и возвращают ответ. Банальный запрос ответ, но это так кажется с первого взгляда. Эти операции называются 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 канале.
Комментарии (3)
kt97679
21.07.2025 20:50Для получения IP адресов с помощью терминала в Linux стоит использовать утилиту dig.Так как обычным HTTP GET запросом это сделать не возможно.
$ curl -s -H 'accept: application/dns+json' "https://8.8.8.8/resolve?name=ya.ru"|jq -r '.Answer[0].data' 77.88.55.242 $
ExWell_94
Довольно понятно и кратко объяснили,мне было полезно, спасибо!
nifnaf94 Автор
Спасибо большое за ваш комментарий) Ради этого я и начал публиковать статьи здесь. В скором времени выйдет статья про блокирующее и не блокирующее IO в Python. Возможно тоже будет интересно вам.