ЕГЭ, ОГЭ, программирование и робототехника в Калининграде

Работа с модулем ipaddress

В предыдущей статье мы познакомились с моделью OSI и узнали, как данные путешествуют по сети. Мы выяснили, что каждое устройство в сети имеет свой IP-адрес, а для разделения адреса на сетевую и узловую части используется маска подсети. Теперь пришло время научиться работать с этими понятиями на практике.

Для успешного решения 10 задания ЕГЭ по информатике требуется уверенно понимать работу IP-адресации: уметь определять адрес сети по адресу узла и маске, находить широковещательный адрес, вычислять количество хостов в сети. Часть заданий можно решать вручную, но гораздо эффективнее и быстрее использовать специализированный модуль Python — ipaddress.

В этой статье мы сначала разберёмся с теорией: что такое маска подсети, как работает CIDR-нотация, как вручную находить адрес сети и узла. А затем познакомимся с модулем ipaddress и его основными функциями.

Маска подсети

Мы уже знаем, что IP-адрес состоит из двух частей: адреса сети и адреса узла. Но как понять, где проходит граница между ними? Для этого используется маска подсети.

Маска подсети — это 32-битное число, которое записывается так же, как IP-адрес: четыре числа от 0 до 255, разделённые точками. Например: 255.255.255.0.

Принцип работы маски простой. В двоичном виде маска состоит из единиц и нулей. Единицы указывают на биты, которые относятся к адресу сети, а нули — на биты адреса узла.

Посмотрим на маску 255.255.255.0 в двоичном виде:

11111111.11111111.11111111.00000000

Здесь 24 единицы и 8 нулей. Это значит, что первые 24 бита IP-адреса (три октета) определяют сеть, а последние 8 бит (один октет) — конкретное устройство в этой сети.

У маски есть важное ограничение: единицы должны идти строго подряд, без пропусков. Сначала все единицы, потом все нули. Поэтому маска 255.255.0.255 недопустима — в ней нули стоят между единицами.

Из-за этого правила каждый октет маски может принимать только определённые значения:

Никакие другие значения в маске встретиться не могут. Например, число 131 (10000011 в двоичной системе) содержит ноль между единицами, поэтому не может быть частью маски.

CIDR-нотация

Писать маску полностью каждый раз неудобно. Поэтому придумали CIDR-нотацию (Classless Inter-Domain Routing) — компактный способ записи.

В CIDR-нотации после IP-адреса через косую черту указывается префикс — количество единиц в маске. Например:

  • 192.168.1.0/24 означает маску с 24 единицами, то есть 255.255.255.0
  • 10.0.0.0/8 означает маску 255.0.0.0 (8 единиц)
  • 172.16.0.0/16 означает маску 255.255.0.0

Префикс может быть от 0 до 32. Чем больше префикс, тем меньше устройств может быть в сети.

Количество IPv4-адресов в сети равно 2 32 — префикс. Например, сеть /24 содержит 2 8 = 256 адресов.

В обычных задачах ЕГЭ из адресов сети исключают адрес сети и широковещательный адрес. Поэтому для /24 остаётся 254 узла. Особые сети /31 и /32 модуль ipaddress обрабатывает по отдельным правилам.

В таблице ниже приведено соответствие длины префикса, маски подсети, количества адресов в сети и количества хостов в сети.

Вычисляем адреса вручную

Прежде чем переходить к Python, давайте разберёмся, как находить адрес сети, адрес узла и широковещательный адрес вручную. Это поможет понять логику работы модуля ipaddress.

Адрес сети

Допустим, у нас есть устройство с IP-адресом 192.168.1.67 и маской 255.255.255.0. Как найти адрес сети?

Для этого выполняем побитовую конъюнкцию (И) между IP-адресом и маской. Переведём оба числа в двоичный вид и применим операцию И к каждой паре битов:

IP-адрес: 11000000.10101000.00000001.01000011  (192.168.1.67)
И
Маска: 11111111.11111111.11111111.00000000  (255.255.255.0)
=
Адрес сети: 11000000.10101000.00000001.00000000  (192.168.1.0)

Правило простое: где в маске единица — оставляем бит из IP-адреса, где ноль — ставим ноль. Результат: адрес сети — 192.168.1.0.

Адрес хоста

Чтобы найти адрес хоста в сети, нужно сначала инвертировать маску (заменить единицы на нули и наоборот), а затем выполнить побитовую конъюнкцию с IP-адресом:

IP-адрес: 11000000.10101000.00000001.01000011  (192.168.1.67)
И
Инверсия маски: 00000000.00000000.00000000.11111111
=
Номер узла: 00000000.00000000.00000000.01000011  (0.0.0.67)

Адрес хоста — 0.0.0.67. Это устройство номер 67 в сети 192.168.1.0.

Широковещательный адрес

Широковещательный адрес используется для отправки сообщений всем устройствам в сети. Чтобы его найти, выполняем побитовую дизъюнкцию (ИЛИ) между адресом сети и инвертированной маской:

Адрес сети: 11000000.10101000.00000001.00000000  (192.168.1.0)
ИЛИ
Инверсия маски: 00000000.00000000.00000000.11111111(0.0.0.255)
=
Широковещательный: 11000000.10101000.00000001.11111111  (192.168.1.255)

В этой сети широковещательный адрес — 192.168.1.255.

Маска сети

Иногда по адресу сети и адресу узла требуется найти подходящую маску. Важно: одна пара адресов может подходить сразу к нескольким маскам, поэтому нельзя просто посчитать совпадающие биты.

Например, адрес сети 192.168.0.0, адрес узла 192.168.0.67. Переводим в двоичный вид:

Адрес сети: 11000000.10101000.00000000.00000000

Адрес узла:  11000000.10101000.00000000.01000011

Видим, что первые 24 бита одинаковые, а последние 8 — разные. Значит, маска: 255.255.255.0 или /24.

Но необязательно вычислять все эти значения вручную, ведь есть встроенный в Python модуль ipaddress, который содержит множество полезных функций для работы с сетями. Давайте познакомимся с этими функциями поближе.

Модуль ipaddress

Модуль предоставляет три удобные функции-конструктора. Они создают объекты адреса, сети и сетевого интерфейса:

  • ip_address — для работы с отдельным IP-адресом
  • ip_network — для работы с сетью целиком
  • ip_interface — для работы с конкретным устройством в сети

Разберём каждый из них подробно.

Работа с IP-адресом

Функция ip_address создаёт объект IP-адреса. Она принимает адрес в виде строки или целого числа:

from ipaddress import ip_address

ip_1 = ip_address("192.168.1.3")
ip_2 = ip_address(3232235779)

print(f'Из строки: {ip_1}')
print(f'Из числа: {ip_2}')

>>>Из строки: 192.168.1.3
>>>Из числа: 192.168.1.3

Любой IPv4-адрес можно представить 32-битным числом. Функция ip_address поддерживает и IPv6, но в 10 задании ЕГЭ мы работаем с IPv4.

from ipaddress import ip_address

ip = ip_address("192.168.1.3")

print(f'В виде числа: {int(ip)}')
print(f'В виде строки: {str(ip)}')

>>>В виде числа: 3232235779
>>>В виде строки: 192.168.1.3

Для задач ЕГЭ часто нужно получить двоичное представление адреса. Сделать это можно через f-строки:

from ipaddress import ip_address

ip = ip_address("192.168.1.3")
bin_ip = f'{int(ip):032b}'

print(bin_ip)

>>>11000000101010000000000100000011

Запись «:032b» означает: вывести число в двоичном формате (b), дополнив нулями слева до 32 символов (032).

Работа с сетью

Функция ip_network создаёт объект сети. Она принимает адрес сети с маской (в любом формате):

from ipaddress import ip_network

net_1 = ip_network('192.168.1.0/255.255.255.0')
net_2 = ip_network('192.168.1.0/24')

print(net_1)
print(net_2)

>>>192.168.1.0/24
>>>192.168.1.0/24

По умолчанию ip_network работает в строгом режиме и ожидает адрес сети, где все биты узловой части равны нулю. Если передать адрес узла, возникнет ошибка.

Чтобы автоматически обнулить узловую часть, используйте параметр strict=False:

from ipaddress import ip_network

net = ip_network('192.168.1.67/24', strict=False)

print(net)

>>>192.168.1.0/24

Теперь можно получить всю информацию о сети через атрибуты объекта:

from ipaddress import ip_network

net = ip_network('192.168.1.67/29', strict=False)

print(f'Адрес сети: {net.network_address}')
print(f'Широковещательный: {net.broadcast_address}')
print(f'Маска: {net.netmask}')
print(f'Префикс: {net.prefixlen}')
print(f'Всего адресов: {net.num_addresses}')

>>>Адрес сети: 192.168.1.64
>>>Широковещательный: 192.168.1.71
>>>Маска: 255.255.255.248
>>>Префикс: 29
>>>Всего адресов: 8

Метод hosts возвращает генератор узловых адресов. В обычной IPv4-сети он исключает адрес сети и широковещательный адрес; для /31 и /32 в Python действуют специальные правила.

from ipaddress import ip_network

net = ip_network('192.168.1.64/29')

for ip in net.hosts():
    print(ip, end=' ')

>>>192.168.1.65 192.168.1.66 192.168.1.67 192.168.1.68 192.168.1.69 192.168.1.70

Объект IP-сети поддерживает протокол итерации, следовательно, можно как перебирать все адреса в сети в цикле, так и обращаться к ним через квадратные скобки (метод __getitem__).

Переберём все адреса в сети с узлом 192.168.1.3 и префиксом /30:

from ipaddress import ip_network

net = ip_network('192.168.1.2/30', strict=False)

for ip in net:
    print(ip, end=" ")
>>>192.168.1.0 192.168.1.1 192.168.1.2 192.168.1.3

Создадим объект IP-адреса 192.168.1.5 и проверим, принадлежит ли он сети 192.168.1.0/28:

from ipaddress import ip_address, ip_network

ip = ip_address('192.168.1.5')
net = ip_network('192.168.1.0/28')

if ip in net:
    print(f'Узел {ip} принадлежит сети')

>>>Узел 192.168.1.5 принадлежит сети

Для обычной проверки принадлежности достаточно выражения ip in net. Если по условию нужен именно адрес узла, отдельно исключите адрес сети (net.network_address) и широковещательный (net.broadcast_address).

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

Функция ip_interface — самая универсальная. Она создаёт объект, который объединяет информацию об адресе узла и о сети, к которой он принадлежит.

from ipaddress import ip_interface

interface = ip_interface('192.168.1.67/24')

print(f'IP-адрес узла: {interface.ip}')
print(f'Адрес сети: {interface.network.network_address}')
print(f'Широковещательный: {interface.network.broadcast_address}')
print(f'Маска: {interface.network.netmask}')
print(f'Префикс: {interface.network.prefixlen}')

>>>IP-адрес узла: 192.168.1.67
>>>Адрес сети: 192.168.1.0
>>>Широковещательный: 192.168.1.255
>>>Маска: 255.255.255.0
>>>Префикс: 24

В отличие от ip_network, функция ip_interface не требует параметра strict=False — она изначально предназначена для работы с адресами узлов.

С помощью интерфейса удобно проверять, принадлежит ли адрес определённой сети:

from ipaddress import ip_interface, ip_address

interface = ip_interface('192.168.1.67/24')
test_ip = ip_address('192.168.1.100')

if test_ip in interface.network:
    print(f'Адрес {test_ip} принадлежит сети {interface.network}')
else:
    print(f'Адрес {test_ip} НЕ принадлежит сети {interface.network}')

>>>Адрес 192.168.1.100 принадлежит сети 192.168.1.0/24

Практические примеры

Давайте закрепим знания на нескольких примерах, типичных для заданий ЕГЭ.

Пример 1: Найти адрес сети

Условие: даны IP-адрес узла 172.16.45.200, маска 255.255.240.0. Найти адрес сети.

Начнём с того, что импортируем нужную функцию из модуля ipaddress. Поскольку нам дан адрес конкретного устройства (узла) и маска, лучше всего подойдёт функция ip_interface:

from ipaddress import ip_interface

Теперь создаём объект интерфейса. Для этого передаём в функцию строку, содержащую IP-адрес и маску через косую черту:

interface = ip_interface('172.16.45.200/255.255.240.0')

В этот момент Python автоматически вычисляет все параметры сети: адрес сети, широковещательный адрес, префикс и так далее. Нам остаётся только обратиться к нужному атрибуту.

Чтобы получить адрес сети, сначала обращаемся к атрибуту network (который содержит информацию о сети), а затем к его атрибуту network_address:

print(f'Адрес сети: {interface.network.network_address}')

Соберём всё вместе и запустим:

from ipaddress import ip_interface

interface = ip_interface('172.16.45.200/255.255.240.0')
print(f'Адрес сети: {interface.network.network_address}')

>>>Адрес сети: 172.16.32.0

Откуда взялось число 32 в третьем октете? Давайте проверим вручную. Маска 255.255.240.0 в двоичном виде выглядит так:

11111111.11111111.11110000.00000000

Третий октет маски — 240, что в двоичном виде равно 11110000. Это значит, что из третьего октета IP-адреса в адрес сети попадают только первые 4 бита.

Третий октет нашего IP-адреса — 45, в двоичном виде это 00101101. Применяем побитовую конъюнкцию с маской:

00101101  (45)
11110000  (240)
──────────────
00100000  (32)

Получаем 32. Именно это значение и показал нам Python.

Пример 2: Определить количество узлов в сети

Условие: дана сеть 10.0.0.0/12. Нужно определить, сколько устройств может быть подключено к этой сети.

Здесь нам дан адрес сети с префиксом, поэтому используем функцию ip_network:

from ipaddress import ip_network

Создаём объект сети, передавая адрес с префиксом:

net = ip_network('10.0.0.0/12')

Теперь можем узнать общее количество адресов в сети через атрибут num_addresses:

print(f'Всего адресов: {net.num_addresses}')

Но не все адреса можно использовать для устройств. Два адреса всегда зарезервированы: адрес сети (первый) и широковещательный адрес (последний). Поэтому для устройств доступно на два адреса меньше:

print(f'Доступно для устройств: {net.num_addresses - 2}')

Полный код:

from ipaddress import ip_network

net = ip_network('10.0.0.0/12')

print(f'Всего адресов: {net.num_addresses}')
print(f'Доступно для устройств: {net.num_addresses - 2}')

>>>Всего адресов: 1048576
>>>Доступно для устройств: 1048574

Проверим это вычисление. Префикс /12 означает, что 12 бит отведены под адрес сети, а оставшиеся 32 — 12 = 20 бит — под адреса узлов. Количество возможных комбинаций из 20 бит равно 220 = 1 048 576. Всё сходится.

Пример 3: Проверить принадлежность адреса сети

Условие: дана сеть 192.168.100.0/26. Нужно проверить, принадлежит ли ей адрес 192.168.100.70.

Для решения нам понадобятся две функции: ip_network для создания объекта сети и ip_address для создания объекта проверяемого адреса:

from ipaddress import ip_network, ip_address

Создаём объект сети:

net = ip_network('192.168.100.0/26')

Создаём объект IP-адреса, который хотим проверить:

ip = ip_address('192.168.100.70')

Теперь используем оператор in, чтобы проверить, входит ли адрес в сеть. Объект сети поддерживает эту проверку, поэтому можно писать просто ip in net:

if ip in net:
    print(f'{ip} принадлежит сети {net}')
else:
    print(f'{ip} НЕ принадлежит сети {net}')

Полный код:

from ipaddress import ip_network, ip_address

net = ip_network('192.168.100.0/26')
ip = ip_address('192.168.100.70')

if ip in net:
    print(f'{ip} принадлежит сети {net}')
else:
    print(f'{ip} НЕ принадлежит сети {net}')

>>>192.168.100.70 НЕ принадлежит сети 192.168.100.0/26

Почему адрес не принадлежит сети? Давайте разберёмся. Префикс /26 означает, что под адреса узлов отведено 32 — 26 = 6 бит. Это даёт 26 = 64 адреса в сети.

Сеть начинается с адреса 192.168.100.0, значит, она содержит адреса от 192.168.100.0 до 192.168.100.63 (всего 64 адреса: от 0 до 63 включительно).

Адрес 192.168.100.70 больше 63, поэтому он выходит за пределы этой сети. Скорее всего, он принадлежит следующей сети — 192.168.100.64/26.

Давайте это проверим:

from ipaddress import ip_network, ip_address

net = ip_network('192.168.100.64/26')
ip = ip_address('192.168.100.70')

if ip in net:
    print(f'{ip} принадлежит сети {net}')
else:
    print(f'{ip} НЕ принадлежит сети {net}')

>>>192.168.100.70 принадлежит сети 192.168.100.64/26

Теперь адрес найден в правильной сети.

Пример 4: Найти все параметры сети по адресу узла

Условие: известно, что компьютер имеет IP-адрес 192.168.50.139 и маску 255.255.255.192. Найти адрес сети, широковещательный адрес, диапазон доступных адресов для устройств.

Это комплексная задача, где нужно получить сразу несколько параметров. Используем ip_interface:

from ipaddress import ip_interface

Создаём объект интерфейса:

interface = ip_interface('192.168.50.139/255.255.255.192')

Через атрибут network получаем доступ ко всем параметрам сети. Выведем их по очереди:

# Адрес сети
print(f'Адрес сети: {interface.network.network_address}')

# Широковещательный адрес
print(f'Широковещательный адрес: {interface.network.broadcast_address}')

# Маска в виде префикса
print(f'Префикс: /{interface.network.prefixlen}')

# Количество адресов
print(f'Всего адресов в сети: {interface.network.num_addresses}')

Чтобы найти диапазон доступных адресов, воспользуемся методом hosts. Он возвращает генератор всех адресов, которые можно назначить устройствам (без адреса сети и широковещательного). Преобразуем генератор в список, чтобы взять первый и последний элементы:

# Получаем список всех доступных адресов
available = list(interface.network.hosts())

# Первый и последний доступные адреса
print(f'Первый доступный: {available[0]}')
print(f'Последний доступный: {available[-1]}')
print(f'Всего доступно для устройств: {len(available)}')
Полный код:
from ipaddress import ip_interface

interface = ip_interface('192.168.50.139/255.255.255.192')

print(f'Адрес сети: {interface.network.network_address}')
print(f'Широковещательный адрес: {interface.network.broadcast_address}')
print(f'Префикс: /{interface.network.prefixlen}')
print(f'Всего адресов в сети: {interface.network.num_addresses}')

available = list(interface.network.hosts())
print(f'Первый доступный: {available[0]}')
print(f'Последний доступный: {available[-1]}')
print(f'Всего доступно для устройств: {len(available)}')

>>>Адрес сети: 192.168.50.128
>>>Широковещательный адрес: 192.168.50.191
>>>Префикс: /26
>>>Всего адресов в сети: 64
>>>Первый доступный: 192.168.50.129
>>>Последний доступный: 192.168.50.190
>>>Всего доступно для устройств: 62

Из результата видно, что наш компьютер с адресом 192.168.50.139 находится в сети, которая начинается с 192.168.50.128 и заканчивается на 192.168.50.191. Для устройств доступны адреса от .129 до .190, то есть 62 адреса.

Итоги

Маска отделяет сетевую часть IPv4-адреса от узловой: единицы относятся к сети, нули — к узлу. Длина префикса /n показывает количество единиц в маске.

Для решения в Python чаще всего нужны ip_address, ip_network(..., strict=False), атрибуты network_address, broadcast_address, netmask, prefixlen и num_addresses.

Перед перебором внимательно определите, о каких адресах говорит условие: обо всех адресах сети или только об узлах. Во втором случае исключайте адрес сети и широковещательный адрес.

В следующей статье начнём решать задания первых двух типов: подбирать маску по адресам узлов и определять, находятся ли адреса в одной сети.

Разберём эту тему на бесплатном пробном занятии

Преподаватель покажет, как решать такие задачи, и подскажет, с чего начать подготовку. Группы до 8 человек, очно в Калининграде и онлайн.

Записаться на пробное занятие