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

Комментарии (3)


  1. ExWell_94
    21.07.2025 20:50

    Довольно понятно и кратко объяснили,мне было полезно, спасибо!


    1. nifnaf94 Автор
      21.07.2025 20:50

      Спасибо большое за ваш комментарий) Ради этого я и начал публиковать статьи здесь. В скором времени выйдет статья про блокирующее и не блокирующее IO в Python. Возможно тоже будет интересно вам.


  1. 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
    $