Видите это довольное лицо? Это Майк Муусс - автор, наверное самой часто используемой утилиты ping (недаром он на фотке так радуется).
Некоторое время назад мне самому довелось написать ping, но в виде отдельной функции.
И на мой взгляд, для си-программистов, которые только начинают работать с сетью, это очень полезная утилита для самостоятельной разработки. Почему? Потому, что для разработки этой утилиты нужно научится делать, казалось бы, самые примитивные действия - отправлять и принимать пакет.
ICMP
Давайте сначала вкратце определим требования конкретно для нашей функции ping. Функция нужна нам для проверки целостности и качества соединения между хостами. И для этого нам достаточно отправить ICMP-эхопакет с запросом целевому хосту и получить от нее ответ. Для оценки качества соединения будем использовать время между запросом и получением ответа.
Системные вызовы
Операционная система (в нашем случае Linux) позволяет получать доступ к сетевым устройствам посредством системных вызовов. Для ping мне потребовались следующие вызовы:
socket
- используется для создания сокетаselect
- в нашем случае используется для проверки состояния сокетаsendto
- для отправки данныхrecvfrom
- для получения данныхinet_aton
- для преобразования ip-aдреса в строковом виде в структуруsockaddr_in
- и другие
Реализация
Функция для получения текущего времени
Для начала напишем вспомогательную функцию для получения текущего времени в миллисекундах.
Данные
Далее нужно определиться с тем, какие данные будем отправлять. Объявим структуру, который будет иметь icmp заголовок и поле данных. Эта структура будет служить нашим пакетом.
Напишем функцию для заполнения нашей структуры.
Функция ping
Сигнатура нашей основной функции будет следующим.
На входе мы получаем ip-адрес пингуемого хоста и таймаут ожидания. На выходе результат выполнения и третий аргумент time
куда запишем время пинга хоста.
Тут же, с помощью inet_aton
, преобразуем ip в строковом виде в структуру sockaddr_in
.
Сокет
Далее нам нужно создать сокет путем вызова соответствующей функции.
Директива SOCK_RAW
говорит о том, что мы отправляем “сырой” пакет без использования транспортного уровня (UDP/TCP). С помощью директивы IPPROTO_ICMP
говорим, что мы будем использовать протокол ICMP. Функция socket()
возвращает нам файловый дескриптор, который будем использовать в последующем.
Отправляем пакет
Перед тем как отправить пакет фиксируем время.
И отправляем пакет.
Получаем ответ
Для получения ответа нужно считать принятые данные из сокета. Данные будут записаны в структуру ip_pkt
.
Таймауты
Далее на мой взгляд идет самая сложная часть. Делать вызов recvfrom
, то есть принимать данные из сети, нам нужно только при наличии данных. Для этого нам поможет системный вызов select
, который используется для отслеживания состояния сокета. А именно:
- Я взвожу
select
на требуемый таймаут, после чего происходит блокировка на этой функции до момента пока не появятся данные или не истечет время - Если
select
разблокировался по таймауту, значит время истекло и мы выходим из цикла - Если
select
разблокировался из-за появления данных, то вызываемrecvfrom
и записываем данные в структуруip_pkt
- Если данные адресованы не нам, взводим
select
заново, ведь у нас еще осталось время
Запуск
Компилируем, запускаем ping и видим, что все работает.
Стоп! Но почему для запуска требуется sudo? А потому, что мы используем сырые пакеты. Помните директиву SOCK_RAW
?
Тогда почему стандартная утилита ping не требует sudo? Ничего страшного. Пара нехитрых действий и наш ping тоже запускается без sudo.
Ссылка на репозиторий с исходниками - https://github.com/a-khakimov/ping.
Если есть вопросы или замечания - пишите! Я обязательно постараюсь ответить. Спасибо!