Стек TCP/IP: байты становятся осознанной информацией
В конце прошлой части пакет поступил наверх, в NetworkManager, который работает в Ring 3. Однако перед ним лежал лишь набор байтов без какого-либо смысла. Только стек протоколов, расположенный поверх KNP, умеет распознавать, где заканчивается кадр Ethernet, начинается ARP, скрывается IP-пакет или находится сегмент TCP. Я реализовал этот стек последовательно, уровень за уровнем. Именно такое разделение делает подобную архитектуру понятной и управляемой.
Весь стек работает целиком в Ring 3. Обычный сервис, один цикл обработки, никаких блокировок. KNP передаёт входящие данные наверх, стек разбирает их по уровням, а исходящий трафик проходит тот же путь в обратном направлении.
Слои и распаковка
Полученный пакет напоминает подарок с несколькими слоями упаковки. Каждый уровень снимает только свою оболочку, анализирует нужное поле заголовка и передаёт оставшуюся часть следующему уровню.
Первый уровень - Ethernet. Он проверяет MAC адрес назначения, определяет тип кадра и направляет содержимое либо в ARP, либо в IP. После этого IP анализирует поле Protocol и выбирает следующий обработчик: ICMP, TCP или UDP. Такое распределение по единственному полю называют демультиплексированием. Именно этот механизм определяет маршрут каждого пакета внутри стека.
Каждый уровень отвечает только за собственную задачу. Ethernet ничего не знает об IP-адресах. IP не интересуют номера портов. TCP полностью игнорирует аппаратные адреса. Каждый компонент принимает решение исключительно по информации собственного заголовка, а затем передаёт данные дальше. Передача в противоположном направлении выглядит зеркально: приложение формирует полезную нагрузку, TCP добавляет свой заголовок, затем IP, после него Ethernet, и готовый кадр через KNP отправляется сетевой карте. Благодаря такой структуре стек удалось построить постепенно, не превращая код в единый монолит.
ARP - мост между двумя мирами
ARP решает небольшую, но крайне важную задачу. Ethernet работает только с MAC-адресами. IP, наоборот, использует исключительно IP-адреса. Между этими двумя уровнями необходим переводчик.
Если стеку неизвестен MAC- адрес получателя, он отправляет широковещательный запрос: "Кто использует этот IP-адрес?". Узел-владелец отвечает собственным MAC-адресом. После получения ответа стек сохраняет пару IP–MAC в небольшом ARP-кэше вместе с отметкой времени. Пока запись остаётся актуальной, повторные запросы уже не требуются. Позже срок действия записи заканчивается, после чего стек снова выполняет поиск. Без такого механизма IP-пакеты никогда не добрались бы до физической сети.
Именно здесь находится одно из слабых мест реализации. Пока стек не получил MAC-адрес, отправка активно ожидает ответ примерно одну секунду. Очередь пакетов решила бы проблему гораздо элегантнее, однако текущий вариант оказался проще во время разработки. Нет ничего более постоянного, чем временное. Еще одно из тысяч TODO...
UDP - минимализм, TCP - полноценная система
Над IP располагается транспортный уровень, где пути двух протоколов расходятся.
UDP представляет максимально простую модель. Соединение отсутствует. Протокол отправляет дейтаграмму, после чего работа заканчивается. Заголовок содержит лишь основные поля, включая номера портов. Получатель определяет нужное приложение именно по номеру порта - всё тот же принцип демультиплексирования.
TCP устроен значительно сложнее. Именно этот уровень потребовал больше всего усилий, благо что документации в интернете вагон и маленькая тележка.
Перед передачей первого байта обе стороны выполняют трёхэтапное рукопожатие (handshake). Клиент отправляет SYN с начальным порядковым номером. Сервер отвечает SYN-ACK и одновременно подтверждает полученное значение. Затем клиент передаёт ACK. После завершения этой последовательности соединение переходит в состояние ESTABLISHED. Дальнейшая работа полностью определяется машиной состояний, которая последовательно проводит соединение через все этапы существования.
Каждое соединение хранит собственный Transmission Control Block. Эта структура содержит всю необходимую информацию: номера последовательности, подтверждения, размеры окон и другие служебные данные. Каждый байт получает уникальный порядковый номер. Получатель подтверждает непрерывный диапазон успешно принятых данных. Если подтверждение отсутствует, отправитель повторяет передачу после увеличивающегося интервала ожидания. Такой механизм обеспечивает правильный порядок доставки даже при потере отдельных пакетов. Именно здесь проявляется главное отличие TCP от UDP: ненадёжный обмен пакетами превращается в устойчивый поток данных.
Через весь стек проходит ещё один общий механизм - контрольная сумма. Каждый уровень вычисляет её для собственных данных и записывает результат в заголовок. Получатель повторяет вычисление и сравнивает оба значения. Любое несовпадение указывает на повреждение пакета во время передачи. Ошибка всего в одном байте или неверный вызов htons способен нарушить работу сразу нескольких уровней.
Честный взгляд на реализацию
Основные механизмы TCP уже работают. Реализация поддерживает трёхстороннее рукопожатие, полноценную машину состояний, повторную передачу с увеличением тайм-аута и управление потоком через окно получателя. Код также включает множество небольших деталей, которые исправляют тонкие ситуации, описанные в спецификации: проверку номеров подтверждения, защиту от ложных сбросов соединения и ряд других особенностей. Именно такие мелочи потребовали значительной части времени.
Несколько возможностей пока отсутствуют. Стек не выполняет фрагментацию, поэтому крупные IP-пакеты отправить невозможно. Алгоритмы управления перегрузкой сети ещё не реализованы, а размер окна остаётся постоянным. Пакеты, прибывшие в неправильном порядке, код не сохраняет для последующей сборки, обработчик сразу отбрасывает их и рассчитывает на повторную передачу. Количество одновременных TCP-соединений тоже ограничивают фиксированные значения в коде. Для локальной сети, ping, простого HTTP-запроса или небольшого сервера этого вполне достаточно. Работа в условиях большого Интернета потребует гораздо более сложной реализации. Тем не менее главная цель уже достигнута: стек создан полностью и уверенно выполняет свою задачу.
Если назвать его одним словом - кровопийца!
Что дальше
Теперь система умеет принимать сетевые пакеты, разбирать их и формировать ответы. Однако стек ещё не знает собственный IP-адрес и не умеет получать адрес по имени узла. Эти задачи решают два протокола следующего уровня DHCP и DNS. Именно о них пойдёт речь в следующей части.
Было бы интересно увидеть ваши комментарии и улучшить статьи.
◀ Предыдущая статья · Содержание · Следующая статья ▶
*Система не стоит на месте, поэтому в дальнейшем тексты могут не совпадать с реальным положением