Все мы пользуемся различными приложениями для разных целей. Банковские приложения для операций с денежными средствами, мессенджеры для общения. Они принимают внутрь себя команду от человека и возвращают ответ. Банальный запрос ответ, но это так кажется с первого взгляда. Эти операции называются 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)
 - kt9767921.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. Возможно тоже будет интересно вам.